summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDavid Oberhollenzer <david.oberhollenzer@tele2.at>2018-02-25 14:33:19 +0100
committerDavid Oberhollenzer <david.oberhollenzer@tele2.at>2018-03-24 17:04:20 +0100
commit9a88f7da453eadc72d8f333700dbd80777feecd1 (patch)
tree8a096e37123ece1d20bcb4d0ae8e064bdd39747a /lib
Initial commit
Signed-off-by: David Oberhollenzer <david.oberhollenzer@tele2.at>
Diffstat (limited to 'lib')
-rw-r--r--lib/Makemodule.am13
-rw-r--r--lib/include/service.h62
-rw-r--r--lib/include/telinit.h20
-rw-r--r--lib/include/util.h58
-rw-r--r--lib/src/del_srv_list.c18
-rw-r--r--lib/src/delsrv.c25
-rw-r--r--lib/src/enum_by_name.c15
-rw-r--r--lib/src/opensock.c33
-rw-r--r--lib/src/rdline.c54
-rw-r--r--lib/src/rdsrv.c249
-rw-r--r--lib/src/splitkv.c124
-rw-r--r--lib/src/srv_tsort.c75
-rw-r--r--lib/src/srvscan.c93
-rw-r--r--lib/src/strexpand.c55
14 files changed, 894 insertions, 0 deletions
diff --git a/lib/Makemodule.am b/lib/Makemodule.am
new file mode 100644
index 0000000..bd82e81
--- /dev/null
+++ b/lib/Makemodule.am
@@ -0,0 +1,13 @@
+HEADRS = lib/include/util.h lib/include/service.h lib/include/telinit.h
+
+libinit_a_SOURCES = lib/src/delsrv.c lib/src/rdline.c
+libinit_a_SOURCES += lib/src/splitkv.c lib/src/enum_by_name.c
+libinit_a_SOURCES += lib/src/strexpand.c lib/src/rdsrv.c lib/src/srvscan.c
+libinit_a_SOURCES += lib/src/del_srv_list.c lib/src/srv_tsort.c
+libinit_a_SOURCES += lib/src/opensock.c $(HEADRS)
+libinit_a_CPPFLAGS = $(AM_CPPFLAGS)
+libinit_a_CFLAGS = $(AM_CFLAGS)
+
+EXTRA_DIST += $(HEADRS)
+
+noinst_LIBRARIES += libinit.a
diff --git a/lib/include/service.h b/lib/include/service.h
new file mode 100644
index 0000000..eb92d85
--- /dev/null
+++ b/lib/include/service.h
@@ -0,0 +1,62 @@
+#ifndef SERVICE_H
+#define SERVICE_H
+
+#include <sys/types.h>
+
+enum {
+ SVC_ONCE = 0,
+ SVC_WAIT,
+ SVC_RESPAWN,
+};
+
+enum {
+ TGT_BOOT = 0,
+ TGT_SHUTDOWN,
+ TGT_REBOOT,
+ TGT_CAD,
+
+ TGT_MAX
+};
+
+typedef struct service_t {
+ int type; /* SVC_* service type */
+ int target; /* TGT_* service target */
+ char *name; /* canonical service name */
+ char *desc; /* description string */
+ char **exec; /* command lines to execute */
+ size_t num_exec; /* number of command lines */
+ char *ctty; /* controlling tty or log file */
+
+ char **before;
+ size_t num_before;
+ char **after;
+ size_t num_after;
+
+ pid_t pid;
+ int status; /* process exit status */
+
+ struct service_t *next;
+} service_t;
+
+typedef struct {
+ service_t *targets[TGT_MAX];
+} service_list_t;
+
+/*
+ Read a service from a file.
+*/
+service_t *rdsrv(int dirfd, const char *filename);
+
+void delsrv(service_t *srv);
+
+int srvscan(const char *directory, service_list_t *list);
+
+void del_srv_list(service_list_t *list);
+
+/*
+ Sort a list of services by dependencies.
+*/
+service_t *srv_tsort(service_t *list);
+
+#endif /* SERVICE_H */
+
diff --git a/lib/include/telinit.h b/lib/include/telinit.h
new file mode 100644
index 0000000..72ad871
--- /dev/null
+++ b/lib/include/telinit.h
@@ -0,0 +1,20 @@
+#ifndef TELINIT_H
+#define TELINIT_H
+
+#include "config.h"
+
+#define INITSOCK SOCKDIR "/" "initd.socket"
+
+enum {
+ TI_SHUTDOWN = 1,
+ TI_REBOOT = 2,
+};
+
+typedef struct {
+ int type;
+} ti_msg_t;
+
+int opensock(void);
+
+#endif /* TELINIT_H */
+
diff --git a/lib/include/util.h b/lib/include/util.h
new file mode 100644
index 0000000..bed2ba7
--- /dev/null
+++ b/lib/include/util.h
@@ -0,0 +1,58 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <sys/types.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "config.h"
+
+#ifdef __GNUC__
+ #define NORETURN __attribute__((noreturn))
+#endif
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+typedef struct {
+ const char *name;
+ int value;
+} enum_map_t;
+
+
+/*
+ Read from fd until end-of-file or a line feed is encountered.
+
+ Returns NULL with errno set on failure. Returns NULL with errno
+ cleared if end-of-file is reached.
+
+ The line must be deallocated with free().
+*/
+char *rdline(int fd);
+
+/*
+ Split a line of the shape "key = value" into key and value part.
+
+ The key can contain alphanumeric characters and can be padded with
+ spaces or tabs.
+
+ The value can be either a sequence of alphanumeric characters, period
+ or underscore OR a string in quotation marks. For strings, the
+ quotation marks are removed and escape sequences are processed.
+
+ The value may also be padded with spaces or tabs but the line may not
+ contain anything else after the value, except for spaces, tabs or
+ the '#' symbol which is interpreted as start of a comment.
+*/
+int splitkv(char *line, char **key, char **value);
+
+/*
+ Search through an array of enum_map_t entries to resolve a string to
+ a numeric value. The end of the map is indicated by a sentinel entry
+ with the name set to NULL.
+*/
+const enum_map_t *enum_by_name(const enum_map_t *map, const char *name);
+
+char *strexpand(const char *inp, size_t argc, const char *const *argv);
+
+#endif /* UTIL_H */
+
diff --git a/lib/src/del_srv_list.c b/lib/src/del_srv_list.c
new file mode 100644
index 0000000..916fcb2
--- /dev/null
+++ b/lib/src/del_srv_list.c
@@ -0,0 +1,18 @@
+#include <stdlib.h>
+
+#include "service.h"
+
+void del_srv_list(service_list_t *list)
+{
+ service_t *srv;
+ int i;
+
+ for (i = 0; i < TGT_MAX; ++i) {
+ while (list->targets[i] != NULL) {
+ srv = list->targets[i];
+ list->targets[i] = srv->next;
+
+ delsrv(srv);
+ }
+ }
+}
diff --git a/lib/src/delsrv.c b/lib/src/delsrv.c
new file mode 100644
index 0000000..b660558
--- /dev/null
+++ b/lib/src/delsrv.c
@@ -0,0 +1,25 @@
+#include <stdlib.h>
+
+#include "service.h"
+
+void delsrv(service_t *srv)
+{
+ size_t i;
+
+ for (i = 0; i < srv->num_exec; ++i)
+ free(srv->exec[i]);
+
+ for (i = 0; i < srv->num_before; ++i)
+ free(srv->before[i]);
+
+ for (i = 0; i < srv->num_after; ++i)
+ free(srv->after[i]);
+
+ free(srv->before);
+ free(srv->after);
+ free(srv->name);
+ free(srv->desc);
+ free(srv->exec);
+ free(srv->ctty);
+ free(srv);
+}
diff --git a/lib/src/enum_by_name.c b/lib/src/enum_by_name.c
new file mode 100644
index 0000000..1948eb9
--- /dev/null
+++ b/lib/src/enum_by_name.c
@@ -0,0 +1,15 @@
+#include <string.h>
+
+#include "util.h"
+
+const enum_map_t *enum_by_name(const enum_map_t *map, const char *name)
+{
+ size_t i;
+
+ for (i = 0; map[i].name != NULL; ++i) {
+ if (!strcmp(map[i].name, name))
+ return map + i;
+ }
+
+ return NULL;
+}
diff --git a/lib/src/opensock.c b/lib/src/opensock.c
new file mode 100644
index 0000000..06101ef
--- /dev/null
+++ b/lib/src/opensock.c
@@ -0,0 +1,33 @@
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "telinit.h"
+
+int opensock(void)
+{
+ struct sockaddr_un un;
+ int fd;
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0) {
+ perror("socket");
+ return -1;
+ }
+
+ memset(&un, 0, sizeof(un));
+ un.sun_family = AF_UNIX;
+
+ strcpy(un.sun_path, INITSOCK);
+
+ if (connect(fd, (struct sockaddr *)&un, sizeof(un))) {
+ perror("connect: " INITSOCK);
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
diff --git a/lib/src/rdline.c b/lib/src/rdline.c
new file mode 100644
index 0000000..591c713
--- /dev/null
+++ b/lib/src/rdline.c
@@ -0,0 +1,54 @@
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <ctype.h>
+
+#include "util.h"
+
+char *rdline(int fd)
+{
+ size_t i = 0, bufsiz = 0, newsz;
+ char c, *new, *buffer = NULL;
+ int ret;
+
+ for (;;) {
+ switch (read(fd, &c, 1)) {
+ case 0:
+ if (i == 0) {
+ errno = 0;
+ return NULL;
+ }
+ c = '\0';
+ break;
+ case 1:
+ if (c == '\n')
+ c = '\0';
+ break;
+ default:
+ if (errno == EINTR)
+ continue;
+ goto fail;
+ }
+
+ if (i == bufsiz) {
+ newsz = bufsiz ? bufsiz * 2 : 16;
+ new = realloc(buffer, newsz);
+
+ if (new == NULL)
+ goto fail;
+
+ buffer = new;
+ bufsiz = newsz;
+ }
+
+ buffer[i++] = c;
+ if (c == '\0')
+ break;
+ }
+ return buffer;
+fail:
+ ret = errno;
+ free(buffer);
+ errno = ret;
+ return NULL;
+}
diff --git a/lib/src/rdsrv.c b/lib/src/rdsrv.c
new file mode 100644
index 0000000..52eb19c
--- /dev/null
+++ b/lib/src/rdsrv.c
@@ -0,0 +1,249 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include "service.h"
+#include "util.h"
+
+static const enum_map_t type_map[] = {
+ { "once", SVC_ONCE },
+ { "wait", SVC_WAIT },
+ { "respawn", SVC_RESPAWN },
+ { NULL, 0 },
+};
+
+static const enum_map_t target_map[] = {
+ { "boot", TGT_BOOT },
+ { "shutdown", TGT_SHUTDOWN },
+ { "reboot", TGT_REBOOT },
+ { "ctrlaltdel", TGT_CAD },
+ { NULL, 0 },
+};
+
+static int srv_name(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ (void)filename; (void)lineno;
+ srv->name = arg;
+ return 0;
+}
+
+static int srv_desc(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ (void)filename; (void)lineno;
+ srv->desc = arg;
+ return 0;
+}
+
+static int srv_tty(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ (void)filename; (void)lineno;
+ srv->ctty = arg;
+ return 0;
+}
+
+static int srv_exec(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ char **new = realloc(srv->exec, sizeof(char*) * (srv->num_exec + 1));
+
+ if (new == NULL) {
+ fprintf(stderr, "%s: %zu: out of memory\n", filename, lineno);
+ free(arg);
+ return -1;
+ }
+
+ srv->exec = new;
+ srv->exec[srv->num_exec++] = arg;
+ return 0;
+}
+
+static int srv_before(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ char **new = realloc(srv->before,
+ sizeof(char*) * (srv->num_before + 1));
+
+ if (new == NULL) {
+ fprintf(stderr, "%s: %zu: out of memory\n", filename, lineno);
+ free(arg);
+ return -1;
+ }
+
+ srv->before = new;
+ srv->before[srv->num_before++] = arg;
+ return 0;
+}
+
+static int srv_after(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ char **new = realloc(srv->after, sizeof(char*) * (srv->num_after + 1));
+
+ if (new == NULL) {
+ fprintf(stderr, "%s: %zu: out of memory\n", filename, lineno);
+ free(arg);
+ return -1;
+ }
+
+ srv->after = new;
+ srv->after[srv->num_after++] = arg;
+ return 0;
+}
+
+static int srv_type(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ const enum_map_t *ent = enum_by_name(type_map, arg);
+
+ if (ent == NULL) {
+ fprintf(stderr, "%s: %zu: unknown service type '%s'\n",
+ filename, lineno, arg);
+ return -1;
+ }
+
+ srv->type = ent->value;
+ free(arg);
+ return 0;
+}
+
+static int srv_target(service_t *srv, char *arg,
+ const char *filename, size_t lineno)
+{
+ const enum_map_t *ent = enum_by_name(target_map, arg);
+
+ if (ent == NULL) {
+ fprintf(stderr, "%s: %zu: unknown service target '%s'\n",
+ filename, lineno, arg);
+ return -1;
+ }
+
+ srv->target = ent->value;
+ free(arg);
+ return 0;
+}
+
+
+static const struct {
+ const char *key;
+
+ int (*handle)(service_t *srv, char *arg,
+ const char *filename, size_t lineno);
+} srv_params[] = {
+ { "name", srv_name },
+ { "description", srv_desc },
+ { "exec", srv_exec },
+ { "type", srv_type },
+ { "target", srv_target },
+ { "tty", srv_tty },
+ { "before", srv_before },
+ { "after", srv_after },
+};
+
+
+service_t *rdsrv(int dirfd, const char *filename)
+{
+ const char *arg, *args[1];
+ char *line, *key, *value;
+ size_t i, argc, lineno;
+ service_t *srv;
+ int fd;
+
+ fd = openat(dirfd, filename, O_RDONLY);
+ if (fd < 0) {
+ perror(filename);
+ return NULL;
+ }
+
+ arg = strchr(filename, '@');
+ if (arg != NULL) {
+ args[0] = arg + 1;
+ argc = 1;
+ } else {
+ argc = 0;
+ }
+
+ srv = calloc(1, sizeof(*srv));
+ if (srv == NULL) {
+ fputs("out of memory\n", stderr);
+ close(fd);
+ return NULL;
+ }
+
+ for (lineno = 1; ; ++lineno) {
+ errno = 0;
+ line = rdline(fd);
+
+ if (line == NULL) {
+ if (errno != 0) {
+ fprintf(stderr, "read: %s: %zu: %s\n",
+ filename, lineno, strerror(errno));
+ goto fail;
+ }
+ break;
+ }
+
+ if (splitkv(line, &key, &value)) {
+ if (key == NULL) {
+ fprintf(stderr,
+ "%s: %zu: expected <key> = <value>\n",
+ filename, lineno);
+ } else if (value == NULL) {
+ fprintf(stderr,
+ "%s: %zu: expected value after %s\n",
+ filename, lineno, key);
+ } else {
+ fprintf(stderr,
+ "%s: %zu: unexpected arguments "
+ "after key-value pair\n",
+ filename, lineno);
+ }
+ goto fail;
+ }
+
+ if (key == NULL) {
+ free(line);
+ continue;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(srv_params); ++i) {
+ if (!strcmp(srv_params[i].key, key))
+ break;
+ }
+
+ if (i >= ARRAY_SIZE(srv_params)) {
+ fprintf(stderr, "%s: %zu: unknown parameter '%s'\n",
+ filename, lineno, key);
+ goto fail_line;
+ }
+
+ value = strexpand(value, argc, args);
+ if (value == NULL) {
+ fputs("out of memory", stderr);
+ goto fail_line;
+ }
+
+ if (srv_params[i].handle(srv, value, filename, lineno)) {
+ free(value);
+ goto fail_line;
+ }
+
+ free(line);
+ }
+
+ close(fd);
+ return srv;
+fail_line:
+ free(line);
+fail:
+ close(fd);
+ delsrv(srv);
+ return NULL;
+}
diff --git a/lib/src/splitkv.c b/lib/src/splitkv.c
new file mode 100644
index 0000000..6c6fe94
--- /dev/null
+++ b/lib/src/splitkv.c
@@ -0,0 +1,124 @@
+#include <ctype.h>
+
+#include "util.h"
+
+static char *skpspc(char *ptr)
+{
+ while (*ptr == ' ' || *ptr == '\t')
+ ++ptr;
+ return ptr;
+}
+
+static int xdigit(int x)
+{
+ if (isupper(x))
+ return x - 'A' + 0x0A;
+ if (islower(x))
+ return x - 'a' + 0x0A;
+ return x - '0';
+}
+
+static char *parse_str(char *src)
+{
+ char *dst = src;
+ int c;
+
+ for (;;) {
+ c = *(src++);
+
+ switch (c) {
+ case '\\':
+ c = *(src++);
+
+ switch (c) {
+ case 'a': c = '\a'; break;
+ case 'b': c = '\b'; break;
+ case 'f': c = '\f'; break;
+ case 'n': c = '\n'; break;
+ case 't': c = '\t'; break;
+ case '\\': break;
+ case '"': break;
+ case 'x':
+ c = 0;
+ if (isxdigit(*src))
+ c = (c << 4) | xdigit(*(src++));
+ if (isxdigit(*src))
+ c = (c << 4) | xdigit(*(src++));
+ break;
+ case '0':
+ c = 0;
+ if (isdigit(*src) && *src < '8')
+ c = (c << 3) | (*(src++) - '0');
+ if (isdigit(*src) && *src < '8')
+ c = (c << 3) | (*(src++) - '0');
+ if (isdigit(*src) && *src < '8')
+ c = (c << 3) | (*(src++) - '0');
+ break;
+ default:
+ return NULL;
+ }
+ break;
+ case '"':
+ *(dst++) = '\0';
+ goto out;
+ }
+
+ *(dst++) = c;
+ }
+out:
+ return src;
+}
+
+int splitkv(char *line, char **key, char **value)
+{
+ *key = NULL;
+ *value = NULL;
+
+ line = skpspc(line);
+
+ if (*line == '#' || *line == '\0')
+ return 0;
+
+ if (!isalpha(*line))
+ return -1;
+
+ *key = line;
+
+ while (isalnum(*line))
+ ++line;
+
+ if (*line == ' ' || *line == '\t') {
+ *(line++) = '\0';
+ line = skpspc(line);
+ }
+
+ if (*line != '=')
+ return -1;
+
+ *(line++) = '\0';
+ line = skpspc(line);
+
+ if (*line == '"') {
+ ++line;
+ *value = line;
+
+ line = parse_str(line);
+ } else if (isalnum(*line)) {
+ *value = line;
+
+ while (isalnum(*line) || *line == '.' || *line == '_')
+ ++line;
+
+ if (*line != '\0')
+ *(line++) = '\0';
+ } else {
+ return -1;
+ }
+
+ line = skpspc(line);
+
+ if (*line != '\0' && *line != '#')
+ return -1;
+
+ return 0;
+}
diff --git a/lib/src/srv_tsort.c b/lib/src/srv_tsort.c
new file mode 100644
index 0000000..e2549b1
--- /dev/null
+++ b/lib/src/srv_tsort.c
@@ -0,0 +1,75 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <errno.h>
+
+#include "service.h"
+
+static bool has_dependencies(service_t *list, service_t *svc)
+{
+ size_t i;
+
+ while (list != NULL) {
+ for (i = 0; i < svc->num_after; ++i) {
+ if (!strcmp(svc->after[i], list->name))
+ return true;
+ }
+
+ for (i = 0; i < list->num_before; ++i) {
+ if (!strcmp(list->before[i], svc->name))
+ return true;
+ }
+
+ list = list->next;
+ }
+
+ return false;
+}
+
+service_t *srv_tsort(service_t *list)
+{
+ service_t *nl = NULL, *end = NULL;
+ service_t *svc, *prev;
+
+ while (list != NULL) {
+ /* remove first service without dependencies */
+ prev = NULL;
+ svc = list;
+
+ while (svc != NULL) {
+ if (has_dependencies(list, svc)) {
+ prev = svc;
+ svc = svc->next;
+ } else {
+ if (prev != NULL) {
+ prev->next = svc->next;
+ } else {
+ list = svc->next;
+ }
+ svc->next = NULL;
+ break;
+ }
+ }
+
+ /* cycle! */
+ if (svc == NULL) {
+ if (end == NULL) {
+ nl = list;
+ } else {
+ end->next = list;
+ }
+ errno = ELOOP;
+ break;
+ }
+
+ /* append to new list */
+ if (end == NULL) {
+ nl = end = svc;
+ } else {
+ end->next = svc;
+ end = svc;
+ }
+ }
+
+ return nl;
+}
diff --git a/lib/src/srvscan.c b/lib/src/srvscan.c
new file mode 100644
index 0000000..71ad4f9
--- /dev/null
+++ b/lib/src/srvscan.c
@@ -0,0 +1,93 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stddef.h>
+#include <dirent.h>
+#include <string.h>
+#include <errno.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <ctype.h>
+
+#include "service.h"
+
+int srvscan(const char *directory, service_list_t *list)
+{
+ int i, dfd, type, ret = 0;
+ struct dirent *ent;
+ const char *ptr;
+ service_t *srv;
+ struct stat sb;
+ DIR *dir;
+
+ for (i = 0; i < TGT_MAX; ++i)
+ list->targets[i] = NULL;
+
+ dir = opendir(directory);
+ if (dir == NULL) {
+ perror(directory);
+ return -1;
+ }
+
+ dfd = dirfd(dir);
+ if (dfd < 0) {
+ perror(directory);
+ closedir(dir);
+ return -1;
+ }
+
+ for (;;) {
+ errno = 0;
+ ent = readdir(dir);
+
+ if (ent == NULL) {
+ if (errno != 0) {
+ perror(directory);
+ ret = -1;
+ }
+ break;
+ }
+
+ for (ptr = ent->d_name; isalnum(*ptr) || *ptr == '_'; ++ptr)
+ ;
+
+ if (*ptr != '\0' && *ptr != '@')
+ continue;
+
+ if (fstatat(dfd, ent->d_name, &sb, AT_SYMLINK_NOFOLLOW)) {
+ fprintf(stderr, "stat %s/%s: %s\n",
+ directory, ent->d_name, strerror(errno));
+ ret = -1;
+ continue;
+ }
+
+ type = (sb.st_mode & S_IFMT);
+
+ if (type != S_IFREG && type != S_IFLNK)
+ continue;
+
+ srv = rdsrv(dfd, ent->d_name);
+ if (srv == NULL) {
+ ret = -1;
+ continue;
+ }
+
+ srv->next = list->targets[srv->target];
+ list->targets[srv->target] = srv;
+ }
+
+ for (i = 0; i < TGT_MAX; ++i) {
+ if (list->targets[i] == NULL)
+ continue;
+
+ errno = 0;
+ list->targets[i] = srv_tsort(list->targets[i]);
+
+ if (errno != 0) {
+ fprintf(stderr, "sorting services read from %s: %s\n",
+ directory, strerror(errno));
+ }
+ }
+
+ closedir(dir);
+ return ret;
+}
diff --git a/lib/src/strexpand.c b/lib/src/strexpand.c
new file mode 100644
index 0000000..7e552eb
--- /dev/null
+++ b/lib/src/strexpand.c
@@ -0,0 +1,55 @@
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+#include "util.h"
+
+char *strexpand(const char *inp, size_t argc, const char *const *argv)
+{
+ char *out, *dst;
+ const char *ptr;
+ size_t i, len;
+
+ ptr = inp;
+ len = 0;
+
+ while (*ptr != '\0') {
+ if (ptr[0] == '%' && isdigit(ptr[1])) {
+ i = ptr[1] - '0';
+ if (i < argc)
+ len += strlen(argv[i]);
+ ptr += 2;
+ } else if (ptr[0] == '%' && ptr[1] == '%') {
+ ptr += 2;
+ len += 1;
+ } else {
+ ++ptr;
+ ++len;
+ }
+ }
+
+ out = calloc(1, len + 1);
+ if (out == NULL)
+ return NULL;
+
+ dst = out;
+
+ while (*inp != '\0') {
+ if (inp[0] == '%' && isdigit(inp[1])) {
+ i = inp[1] - '0';
+ if (i < argc) {
+ len = strlen(argv[i]);
+ memcpy(dst, argv[i], len);
+ dst += len;
+ }
+ inp += 2;
+ } else if (inp[0] == '%' && inp[1] == '%') {
+ *(dst++) = '%';
+ inp += 2;
+ } else {
+ *(dst++) = *(inp++);
+ }
+ }
+
+ return out;
+}