From 7dc2266c2f225b57aa676a3ed3f3869096cef78f Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Sat, 24 Dec 2022 11:15:40 -0800 Subject: [PATCH] WIP: add python/rust style format strings --print-format currently uses printf-style format strings, which have limited namespace (a-zA-Z), low flexibility (depends-with-version and depends-without-version have to be different sequences), and are difficult to remember (is %d depends or description?). Python/Rust format strings allow full word substitutions. In addition, the mfmt.c library being used makes it possible to extend the standard formatting for much greater control. For example, we can allow nested templates so that instead of having "{depends}" and "{depends-without-version}" variants, we can allow something like "{depends<{name}={version}: {description}>}". For example: pacman -Sp --pformat="{name}: {description} ({packager})" pacman --- lib/libalpm/alpm.h | 3 + lib/libalpm/info.c | 226 ++++++++++++++++++++++++++++++++++++++++ lib/libalpm/meson.build | 2 + lib/libalpm/mfmt.c | 193 ++++++++++++++++++++++++++++++++++ lib/libalpm/mfmt.h | 66 ++++++++++++ src/pacman/conf.h | 2 + src/pacman/pacman.c | 4 + src/pacman/util.c | 4 + 8 files changed, 500 insertions(+) create mode 100644 lib/libalpm/info.c create mode 100644 lib/libalpm/mfmt.c create mode 100644 lib/libalpm/mfmt.h diff --git a/lib/libalpm/alpm.h b/lib/libalpm/alpm.h index 07e16b9f..7c89cb5a 100644 --- a/lib/libalpm/alpm.h +++ b/lib/libalpm/alpm.h @@ -2913,6 +2913,9 @@ int alpm_capabilities(void); /* End of libalpm_api */ /** @} */ +size_t alpm_info_print_pkg(const char *format, alpm_pkg_t *pkg); +size_t alpm_info_print_pkgs(const char *format, alpm_list_t *pkgs); + #ifdef __cplusplus } #endif diff --git a/lib/libalpm/info.c b/lib/libalpm/info.c new file mode 100644 index 00000000..097f6db1 --- /dev/null +++ b/lib/libalpm/info.c @@ -0,0 +1,226 @@ +#include +#include + +#include "alpm.h" +#include "util.h" + +#include "mfmt.h" + +typedef enum field_t { + FILENAME, + NAME, + BASE, + DESCRIPTION, + VERSION, + ORIGIN, + REASON, + LICENSE, + GROUP, + + DEPENDS, + OPTDEPENDS, + CONFLICTS, + PROVIDES, + REPLACES, + REQUIREDBY, + + DELTAS, + FILES, + BACKUP, + DB, + VALIDATION, + URL, + BUILDDATE, + INSTALLDATE, + PACKAGER, + MD5SUM, + SHA256SUM, + ARCH, + SIZE, + ISIZE, + BASE64SIG, + + UNKNOWN, +} field_t; + +static struct field_map_t { + const char *input; + field_t field; +} field_map[] = { + {"filename", FILENAME}, + {"name", NAME}, + {"base", BASE}, + {"description", DESCRIPTION}, + {"version", VERSION}, + + {"license", LICENSE}, + {"group", GROUP}, + {"groups", GROUP}, + + {"depends", DEPENDS}, + {"optdepends", OPTDEPENDS}, + {"conflicts", CONFLICTS}, + {"provides", PROVIDES}, + {"replaces", REPLACES}, + {"requiredby", REQUIREDBY}, + + {"url", URL}, + {"builddate", BUILDDATE}, + {"installdate", INSTALLDATE}, + {"packager", PACKAGER}, + {"md5sum", MD5SUM}, + {"sha256sum", SHA256SUM}, + {"arch", ARCH}, + {"size", SIZE}, + {"isize", ISIZE}, + {"base64sig", BASE64SIG}, + + {NULL, 0} +}; + +static char *_alpm_hr_size(off_t bytes, char *dest) +{ + static const char *suff[] = {"B", "K", "M", "G", "T", "P", "E", NULL}; + float hrsize; + int s = 0; + while((bytes >= 1000000 || bytes <= -1000000) && suff[s + 1]) { + bytes /= 1024; + ++s; + } + hrsize = bytes; + if((hrsize >= 1000 || hrsize <= -1000) && suff[s + 1]) { + hrsize /= 1024; + ++s; + } + sprintf(dest, "%.2f %s", hrsize, suff[s]); + return dest; +} + +static field_t _alpm_info_lookup_field(const char *name) { + struct field_map_t *m; + for(m = field_map; m->input; m++) { + if(strcmp(name, m->input) == 0) { return m->field; } + } + return UNKNOWN; +} + +static size_t _alpm_info_print_str(mfmt_token_callback_t *t, const char *str, FILE *f) { + return mfmt_render_str(t, str ? str : "NULL", f); +} + +static size_t _alpm_info_print_size(mfmt_token_callback_t *t, const off_t s, FILE *f) { + if(s) { + char hrsize[50]; + if(t->conversion == 'd') { + snprintf(hrsize, 50, "%lld", (long long)s); + } else { + _alpm_hr_size(s, hrsize); + } + return mfmt_render_str(t, hrsize, f); + } else { + return mfmt_render_str(t, "NULL", f); + } +} + +static size_t _alpm_info_print_strlist(mfmt_token_callback_t *t, alpm_list_t *l, FILE *f) { + if(l) { + size_t len = 0; + while(l) { + len += mfmt_render_str(t, l->data, f) + 1; + fputc('\n', f); + l = l->next; + } + return len; + } else { + return mfmt_render_str(t, "NULL", f); + } +} + +static size_t _alpm_info_print_deplist(mfmt_token_callback_t *t, alpm_list_t *l, FILE *f) { + if(l) { + size_t len = 0; + while(l) { + char *s = alpm_dep_compute_string(l->data); + len += mfmt_render_str(t, s, f) + 1; + fputc('\n', f); + l = l->next; + free(s); + } + return len; + } else { + return mfmt_render_str(t, "NULL", f); + } +} + +static size_t _alpm_info_print_timestamp(mfmt_token_callback_t *t, const alpm_time_t s, FILE *f) { + if(s) { + char datestr[50] = ""; + if(strftime(datestr, 50, " %c", localtime(&s)) == 0) { return 0; } + return mfmt_render_str(t, datestr + 1, f); + } else { + return mfmt_render_str(t, "NULL", f); + } +} + +static size_t _alpm_info_process_token(FILE *f, mfmt_token_callback_t *t, void *ctx, void *arg) { + alpm_pkg_t *p = arg; + (void)ctx; + switch(_alpm_info_lookup_field(t->name)) { + case NAME: return _alpm_info_print_str(t, alpm_pkg_get_name(p), f); + case DESCRIPTION: return _alpm_info_print_str(t, alpm_pkg_get_desc(p), f); + case PACKAGER: return _alpm_info_print_str(t, alpm_pkg_get_packager(p), f); + case MD5SUM: return _alpm_info_print_str(t, alpm_pkg_get_md5sum(p), f); + case FILENAME: return _alpm_info_print_str(t, alpm_pkg_get_filename(p), f); + case BASE: return _alpm_info_print_str(t, alpm_pkg_get_base(p), f); + case VERSION: return _alpm_info_print_str(t, alpm_pkg_get_version(p), f); + case URL: return _alpm_info_print_str(t, alpm_pkg_get_url(p), f); + case SHA256SUM: return _alpm_info_print_str(t, alpm_pkg_get_sha256sum(p), f); + case ARCH: return _alpm_info_print_str(t, alpm_pkg_get_arch(p), f); + case BASE64SIG: return _alpm_info_print_str(t, alpm_pkg_get_base64_sig(p), f); + + case SIZE: return _alpm_info_print_size(t, alpm_pkg_get_size(p), f); + case ISIZE: return _alpm_info_print_size(t, alpm_pkg_get_isize(p), f); + + case BUILDDATE: return _alpm_info_print_timestamp(t, alpm_pkg_get_builddate(p), f); + case INSTALLDATE: return _alpm_info_print_timestamp(t, alpm_pkg_get_installdate(p), f); + + case DEPENDS: return _alpm_info_print_deplist(t, alpm_pkg_get_depends(p), f); + case OPTDEPENDS: return _alpm_info_print_deplist(t, alpm_pkg_get_optdepends(p), f); + case CONFLICTS: return _alpm_info_print_deplist(t, alpm_pkg_get_conflicts(p), f); + case PROVIDES: return _alpm_info_print_deplist(t, alpm_pkg_get_provides(p), f); + case REPLACES: return _alpm_info_print_deplist(t, alpm_pkg_get_replaces(p), f); + case REQUIREDBY: { + alpm_list_t *rb = alpm_pkg_compute_requiredby(p); + size_t len = _alpm_info_print_strlist(t, rb, f); + FREELIST(rb); + return len; + } + + default: errno = EINVAL; return 0; + } +} + +size_t SYMEXPORT alpm_info_print_pkg(const char *format, alpm_pkg_t *pkg) { + alpm_list_t l = { + .data = pkg, + .next = NULL, + }; + l.prev = &l; + return alpm_info_print_pkgs(format, &l); +} + +size_t SYMEXPORT alpm_info_print_pkgs(const char *format, alpm_list_t *pkgs) { + mfmt_t *mfmt = mfmt_parse(format, _alpm_info_process_token, NULL); + size_t len = 0; + if(mfmt == NULL) { + return 0; + } + for(alpm_list_t *i = pkgs; i; i = i->next) { + size_t plen = mfmt_printf(mfmt, i->data, stdout); + if(plen == 0) { return 0; } + len += plen; + } + return len; +} + +/* vim: set ts=2 sw=2 et: */ diff --git a/lib/libalpm/meson.build b/lib/libalpm/meson.build index 607e91a3..c6311aed 100644 --- a/lib/libalpm/meson.build +++ b/lib/libalpm/meson.build @@ -28,5 +28,7 @@ libalpm_sources = files(''' sync.h sync.c trans.h trans.c util.h util.c + info.c + mfmt.c mfmt.h version.c '''.split()) diff --git a/lib/libalpm/mfmt.c b/lib/libalpm/mfmt.c new file mode 100644 index 00000000..c55d3fd0 --- /dev/null +++ b/lib/libalpm/mfmt.c @@ -0,0 +1,193 @@ +#define _GNU_SOURCE + +#include +#include +#include + +#include "mfmt.h" + +char *_mfmt_find_unescaped_char(char *haystack, char needle) { + while(1) { + haystack = strchrnul(haystack, needle); + if(*haystack && *(haystack + 1) == needle) { haystack += 2; continue; } + else { break; } + } + return haystack; +} + +void _mfmt_brace_dedup(char *str) { + char *c = str, *end = str + strlen(str); + while((c = strchr(c, '{'))) { + memmove(c, c + 1, end - c); + c++; + } + + c = str; + while((c = strchr(c, '}'))) { + memmove(c, c + 1, end - c); + c++; + } +} + +mfmt_t *mfmt_parse(const char *tmpl, mfmt_callback_t *cb, void *ctx) { + mfmt_t *mfmt; + char *c; + + mfmt = calloc(sizeof(mfmt_t), 1); + if(mfmt == NULL) { return NULL; } + + mfmt->cb = cb; + mfmt->ctx = ctx; + + for(c = (char*) tmpl; c && *c; ) { + mfmt->token_count++; + if(*c == '{' && *(c + 1) != '{') { + /* replacement */ + if(!*(c = _mfmt_find_unescaped_char(c + 1, '}'))) { + errno = EINVAL; + free(mfmt); + return NULL; + } else { + c++; + } + } else { + /* literal */ + c = _mfmt_find_unescaped_char(c, '{'); + } + } + + if((mfmt->tokens = calloc(sizeof(mfmt_token_t), mfmt->token_count)) == NULL) { + free(mfmt); + return NULL; + } + + size_t i; + for(c = (char*) tmpl, i = 0; c && *c; i++) { + if(*c == '{' && *(c + 1) != '{') { + /* replacement */ + mfmt_token_callback_t *t = &mfmt->tokens[i].callback; + char *end = _mfmt_find_unescaped_char(c + 1, '}'); + t->type = MFMT_TOKEN_CALLBACK; + t->name = strndup(c + 1, end - c - 1); + c = end + 1; + } else { + /* literal */ + char *end = _mfmt_find_unescaped_char(c, '{'); + mfmt_token_literal_t *t = &mfmt->tokens[i].literal; + t->type = MFMT_TOKEN_LITERAL; + t->string = strndup(c, end - c); + _mfmt_brace_dedup(t->string); + c = end; + } + } + + return mfmt; +} + +size_t mfmt_printf(mfmt_t *mfmt, void *args, FILE *f) { + size_t len = 0; + size_t i; + for(i = 0; i < mfmt->token_count; i++) { + mfmt_token_t *t = &mfmt->tokens[i]; + switch(t->base.type) { + case MFMT_TOKEN_LITERAL: + len += fputs(t->literal.string, f); + break; + case MFMT_TOKEN_CALLBACK: + len += mfmt->cb(f, &t->callback, mfmt->ctx, args); + break; + default: + errno = EINVAL; + return 0; + } + } + return len; +} + +static size_t _mfmt_printf_close(mfmt_t *mfmt, void *args, FILE *f) { + if(f) { + size_t len = mfmt_printf(mfmt, args, f); + fclose(f); + return len; + } + return -1; +} + +size_t mfmt_printd(mfmt_t *mfmt, void *args, int fd) { + return _mfmt_printf_close(mfmt, args, fdopen(fd, "w")); +} + +size_t mfmt_printb(mfmt_t *mfmt, void *args, char *buf, size_t buflen) { + return _mfmt_printf_close(mfmt, args, fmemopen(buf, buflen, "w")); +} + +size_t mfmt_prints(mfmt_t *mfmt, void *args, char **buf, size_t *buflen) { + return _mfmt_printf_close(mfmt, args, open_memstream(buf, buflen)); +} + +size_t mfmt_fmt(const char *tmpl, mfmt_val_t *args, FILE *f) { + mfmt_t *mfmt = mfmt_parse(tmpl, NULL, NULL); + size_t len; + for(size_t i = 0; i < mfmt->token_count; i++) { + mfmt_token_t *t = &mfmt->tokens[i]; + switch(t->base.type) { + case MFMT_TOKEN_LITERAL: + len += fputs(t->literal.string, f); + break; + case MFMT_TOKEN_CALLBACK: + /* fprintf(stderr, "token: %s\n", t->callback.name); */ + if(t->callback.name[0]) { + for(mfmt_val_t *v = args; v; v++) { + /* fprintf(stderr, "val: %s\n", v->name); */ + if(strcmp(v->name, t->callback.name) == 0) { + len += fputs(v->string, f); + break; + } + } + } else { + len += fputs(args->string, f); + args++; + } + break; + } + } + return len; +} + +size_t mfmt_mfmt(mfmt_t *mfmt, mfmt_val_t *args, FILE *f) { + size_t len; + for(size_t i = 0; i < mfmt->token_count; i++) { + mfmt_token_t *t = &mfmt->tokens[i]; + switch(t->base.type) { + case MFMT_TOKEN_LITERAL: + len += fputs(t->literal.string, f); + break; + case MFMT_TOKEN_CALLBACK: + /* fprintf(stderr, "token: %s\n", t->callback.name); */ + if(t->callback.name[0]) { + for(mfmt_val_t *v = args; v; v++) { + /* fprintf(stderr, "val: %s\n", v->name); */ + if(strcmp(v->name, t->callback.name) == 0) { + len += fputs(v->string, f); + break; + } + } + } else { + len += fputs(args->string, f); + args++; + } + break; + } + } + return len; +} + +size_t mfmt_render_int(mfmt_token_callback_t *t, const intmax_t i, FILE *f) { + (void)t; + return fprintf(f, "%jd", i); +} + +size_t mfmt_render_str(mfmt_token_callback_t *t, const char *str, FILE *f) { + (void)t; + return fputs(str, f); +} diff --git a/lib/libalpm/mfmt.h b/lib/libalpm/mfmt.h new file mode 100644 index 00000000..2f90cdb7 --- /dev/null +++ b/lib/libalpm/mfmt.h @@ -0,0 +1,66 @@ +#include +#include +#include + +typedef enum mfmt_token_type_t { + MFMT_TOKEN_LITERAL, + MFMT_TOKEN_CALLBACK, +} mfmt_token_type_t; + +typedef struct mfmt_token_literal_t { + mfmt_token_type_t type; + char *string; +} mfmt_token_literal_t; + +typedef struct mfmt_token_base_t { + mfmt_token_type_t type; +} mfmt_token_base_t; + +typedef struct mfmt_token_callback_t { + mfmt_token_type_t type; + + size_t position; + char *name; + size_t width; + size_t precision; + char align; + char fill; + char conversion; + int sign; +} mfmt_token_callback_t; + +typedef union mfmt_token_t { + mfmt_token_base_t base; + mfmt_token_literal_t literal; + mfmt_token_callback_t callback; +} mfmt_token_t; + +typedef size_t (mfmt_callback_t)(FILE *f, mfmt_token_callback_t *token, void *ctx, void *args); + +typedef struct mfmt_t { + mfmt_callback_t *cb; + void *ctx; + size_t token_count; + mfmt_token_t *tokens; +} mfmt_t; + +typedef struct mfmt_val_t { + const char *name; + const char *string; +} mfmt_val_t; + +mfmt_t *mfmt_parse(const char *tmpl, mfmt_callback_t *cb, void *ctx); +size_t mfmt_printf(mfmt_t *mfmt, void *args, FILE *f); +size_t mfmt_printd(mfmt_t *mfmt, void *args, int fd); +size_t mfmt_printb(mfmt_t *mfmt, void *args, char *buf, size_t buflen); +size_t mfmt_prints(mfmt_t *mfmt, void *args, char **buf, size_t *buflen); +void mfmt_free(mfmt_t *mfmt); + +size_t mfmt_render_int(mfmt_token_callback_t *token, intmax_t i, FILE *f); +size_t mfmt_render_uint(mfmt_token_callback_t *token, uintmax_t i, FILE *f); +size_t mfmt_render_str(mfmt_token_callback_t *token, const char *str, FILE *f); + +size_t mfmt_formatf(const char *tmpl, mfmt_callback_t *cb, void *ctx, FILE *f); +size_t mfmt_formatd(const char *tmpl, mfmt_callback_t *cb, void *ctx, int fd); +size_t mfmt_formatb(const char *tmpl, mfmt_callback_t *cb, void *ctx, char *buf, size_t buflen); +size_t mfmt_formats(const char *tmpl, mfmt_callback_t *cb, void *ctx, char **buf); diff --git a/src/pacman/conf.h b/src/pacman/conf.h index f7916ca9..3e9c1aae 100644 --- a/src/pacman/conf.h +++ b/src/pacman/conf.h @@ -58,6 +58,7 @@ typedef struct __config_t { unsigned short color; unsigned short disable_dl_timeout; char *print_format; + char *pformat; /* unfortunately, we have to keep track of paths both here and in the library * because they can come from both the command line or config file, and we * need to ensure we get the order of preference right. */ @@ -172,6 +173,7 @@ enum { OP_ASEXPLICIT, OP_ARCH, OP_PRINTFORMAT, + OP_PFORMAT, OP_GPGDIR, OP_DBONLY, OP_FORCE, diff --git a/src/pacman/pacman.c b/src/pacman/pacman.c index e5c6e420..75ea4231 100644 --- a/src/pacman/pacman.c +++ b/src/pacman/pacman.c @@ -651,6 +651,9 @@ static int parsearg_trans(int opt) free(config->print_format); config->print_format = strdup(optarg); break; + case OP_PFORMAT: + config->pformat = strdup(optarg); + break; case OP_ASSUMEINSTALLED: parsearg_util_addlist(&(config->assumeinstalled)); break; @@ -944,6 +947,7 @@ static int parseargs(int argc, char *argv[]) {"asexplicit", no_argument, 0, OP_ASEXPLICIT}, {"arch", required_argument, 0, OP_ARCH}, {"print-format", required_argument, 0, OP_PRINTFORMAT}, + {"pformat" , required_argument, 0, OP_PFORMAT}, {"gpgdir", required_argument, 0, OP_GPGDIR}, {"dbonly", no_argument, 0, OP_DBONLY}, {"color", required_argument, 0, OP_COLOR}, diff --git a/src/pacman/util.c b/src/pacman/util.c index 5f5c7c54..027397b4 100644 --- a/src/pacman/util.c +++ b/src/pacman/util.c @@ -1168,6 +1168,10 @@ double humanize_size(off_t bytes, const char target_unit, int precision, void print_packages(const alpm_list_t *packages) { const alpm_list_t *i; + if(config->pformat) { + alpm_info_print_pkgs(config->pformat, (alpm_list_t*) packages); + return; + } if(!config->print_format) { config->print_format = strdup("%l"); }