aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeah Neukirchen <leah@vuxu.org>2019-11-20 22:15:56 +0100
committerLeah Neukirchen <leah@vuxu.org>2019-11-20 22:15:56 +0100
commitc602bfd08a47f4840bf2e8d96663f76e2fb22f1c (patch)
tree2262a2f7cc78a3e9ab73fc618f431c7cf66a531a
downloadatxec-c602bfd08a47f4840bf2e8d96663f76e2fb22f1c.tar.gz
atxec-c602bfd08a47f4840bf2e8d96663f76e2fb22f1c.tar.xz
atxec-c602bfd08a47f4840bf2e8d96663f76e2fb22f1c.zip
initial commit
-rw-r--r--Makefile26
-rw-r--r--README47
-rw-r--r--atxec.168
-rwxr-xr-xatxec.c144
-rw-r--r--t/errors.t23
-rw-r--r--t/simple.t99
-rwxr-xr-xtap3112
7 files changed, 519 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..038f4f5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,26 @@
+ALL=atxec
+
+CFLAGS=-g -O2 -Wall -Wno-switch -Wextra -Wwrite-strings
+
+DESTDIR=
+PREFIX=/usr/local
+BINDIR=$(PREFIX)/bin
+MANDIR=$(PREFIX)/share/man
+
+all: $(ALL)
+
+clean: FRC
+ rm -f $(ALL)
+
+check: FRC all
+ prove -v
+
+install: FRC all
+ mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1
+ install -m0755 $(ALL) $(DESTDIR)$(BINDIR)
+ install -m0644 $(ALL:=.1) $(DESTDIR)$(MANDIR)/man1
+
+README: atxec.1
+ mandoc -Tutf8 $< | col -bx >$@
+
+FRC:
diff --git a/README b/README
new file mode 100644
index 0000000..218935b
--- /dev/null
+++ b/README
@@ -0,0 +1,47 @@
+XE(1) General Commands Manual XE(1)
+
+NAME
+ atxec – run command expanding arguments from file or environment
+
+SYNOPSIS
+ atxec command ...
+
+DESCRIPTION
+ The atxec utility constructs and then executes command lines from
+ specified arguments, expanding arguments like
+
+ • ‘@file’ with the contents of file;
+
+ • ‘@$VAR’ with the contents of the environment variable VAR;
+
+ • ‘@@str’ with the string ‘@str’ (for quoting).
+
+ Files and environment variables are split at whitespace into multiple
+ arguments. You can use rc(1)-style quoting, i.e. everything between
+ single quotes is escaped (and regarded as a single argument), and two
+ single quotes in a row expand to a quoted single quote.
+
+ Files can contain comments, text after ‘#’ is ignored (note: the ‘#’ must
+ be preceded by beginning of line or whitespace.)
+
+EXIT STATUS
+ The atxec utility exits 0 on success, and >0 if an error occurs.
+
+EXAMPLES
+ TBD
+
+SEE ALSO
+ execline(1)
+
+AUTHORS
+ Leah Neukirchen <leah@vuxu.org>
+
+LICENSE
+ atxec is in the public domain.
+
+ To the extent possible under law, the creator of this work has waived all
+ copyright and related or neighboring rights to this work.
+
+ http://creativecommons.org/publicdomain/zero/1.0/
+
+Void Linux November 20, 2019 Void Linux
diff --git a/atxec.1 b/atxec.1
new file mode 100644
index 0000000..baac8fb
--- /dev/null
+++ b/atxec.1
@@ -0,0 +1,68 @@
+.Dd November 20, 2019
+.Dt XE 1
+.Os
+.Sh NAME
+.Nm atxec
+.Nd run command expanding arguments from file or environment
+.Sh SYNOPSIS
+.Nm
+.Ar command\ ...
+.Sh DESCRIPTION
+The
+.Nm
+utility constructs and then executes command lines from specified arguments,
+expanding arguments like
+.Bl -bullet
+.It
+.Sq Ic @ Ns Ar file
+with the contents of
+.Ar file ;
+.It
+.Sq Ic @$ Ns Ar VAR
+with the contents of the environment variable
+.Ar VAR ;
+.It
+.Sq Ic @@ Ns Ar str
+with the string
+.Sq @ Ns Ar str
+.Pq for quoting .
+.El
+.Pp
+Files and environment variables are split at whitespace into
+.Em multiple
+arguments.
+You can use
+.Xr rc 1 Ns - Ns style
+quoting, i.e.
+everything between single quotes is escaped
+.Pq and regarded as a single argument ,
+and two single quotes in a row expand to a quoted single quote.
+.Pp
+Files can contain comments,
+text after
+.Sq Ic #
+is ignored
+.Po
+note:
+the
+.Sq Ic #
+must be preceded by beginning of line or whitespace.
+.Pc
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+TBD
+.Sh SEE ALSO
+.Xr execline 1
+.Sh AUTHORS
+.An Leah Neukirchen Aq Mt leah@vuxu.org
+.Sh LICENSE
+.Nm
+is in the public domain.
+.Pp
+To the extent possible under law,
+the creator of this work
+has waived all copyright and related or
+neighboring rights to this work.
+.Pp
+.Lk http://creativecommons.org/publicdomain/zero/1.0/
diff --git a/atxec.c b/atxec.c
new file mode 100755
index 0000000..9432c56
--- /dev/null
+++ b/atxec.c
@@ -0,0 +1,144 @@
+/*
+ * atxec - run command expanding arguments from file or environment
+ *
+ * To the extent possible under law, Leah Neukirchen <leah@vuxu.org>
+ * has waived all copyright and related or neighboring rights to this work.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// '''' => '
+
+// # line comment (space or beginning of line before #)
+// @file
+// @$ENV
+// @@argwithone@
+// fallbacks?
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MAXARGS 2048
+int narg;
+char *args[MAXARGS];
+
+static void
+push_arg(char *s)
+{
+ if (narg >= MAXARGS) {
+ fprintf(stderr, "atxec: too many arguments\n");
+ exit(111);
+ }
+
+ args[narg++] = s;
+}
+
+void
+arg_splice(char *s)
+{
+ char *beg;
+ char *t;
+
+ if (!s)
+ return;
+
+ while (1) {
+ while (isspace(*s) || *s == '#')
+ if (*s == '#') /* skip line comment */
+ while (*s != '\n')
+ s++;
+ else
+ s++;
+
+ if (!*s)
+ break;
+
+ if (*s == '\'') { /* rc-quoted string */
+ s++;
+ beg = t = s;
+
+ while (*s)
+ if (*s == '\'') {
+ *t++ = *s++;
+ if (*s == '\'') {
+ s++;
+ } else {
+ *--t = 0;
+ if (!*s)
+ beg--;
+ break;
+ }
+ } else {
+ *t++ = *s++;
+ }
+ } else { /* bareword, may contain # without whitespace */
+ beg = s;
+ while (*s && !isspace(*s))
+ s++;
+ }
+
+ push_arg(beg);
+
+ if (*s) {
+ *s = 0;
+ s++;
+ }
+ }
+}
+
+void
+file_splice(char *file)
+{
+ struct stat st;
+ char *s;
+
+ FILE *f = fopen(file, "rb");
+ if (!f)
+ return; /* ignore file does not exist */
+
+ fstat(fileno(f), &st);
+
+ s = malloc(st.st_size + 1);
+ if (!s) {
+ fclose(f);
+ return;
+ }
+ fread(s, 1, st.st_size, f);
+ fclose(f);
+
+ s[st.st_size] = 0;
+
+ arg_splice(s);
+
+ /* leak string, args points into it! */
+}
+
+int
+main(int argc, char *argv[])
+{
+ if (argc == 1) {
+ fprintf(stderr, "usage\n");
+ return 1;
+ }
+
+ for (int i = 1; i < argc; i++)
+ if (argv[i][0] == '@') {
+ if (argv[i][1] == '@')
+ push_arg(argv[i]+1);
+ if (argv[i][1] == '$')
+ arg_splice(getenv(argv[i]+2));
+ else
+ file_splice(argv[i]+1);
+ } else {
+ push_arg(argv[i]);
+ }
+
+ execvp(args[0], args);
+
+ perror("argsplice: exec");
+ return 111;
+}
diff --git a/t/errors.t b/t/errors.t
new file mode 100644
index 0000000..b61b47d
--- /dev/null
+++ b/t/errors.t
@@ -0,0 +1,23 @@
+#!/bin/sh
+export "PATH=.:$PATH"
+
+printf '1..3\n'
+printf '# errors\n'
+
+tap3 'no arguments' <<'EOF'
+atxec
+>>>2 /sage/
+>>>= 1
+EOF
+
+tap3 'not found' <<'EOF'
+atxec /doesnotexist
+>>>2 /o such file/
+>>>= 111
+EOF
+
+tap3 'too many arguments' <<'EOF'
+atxec $(yes | sed 99999q)
+>>>2 /too many/
+>>>= 111
+EOF
diff --git a/t/simple.t b/t/simple.t
new file mode 100644
index 0000000..4606da2
--- /dev/null
+++ b/t/simple.t
@@ -0,0 +1,99 @@
+#!/bin/sh
+export "PATH=.:$PATH"
+
+printf '1..12\n'
+printf '# simple tests\n'
+
+tap3 'no expansion' <<'EOF'
+atxec echo 1 2 3
+>>>
+1 2 3
+EOF
+
+tap3 'env expansion' <<'EOF'
+TWO=2 atxec echo 1 '@$TWO' 3
+>>>
+1 2 3
+EOF
+
+tap3 'file expansion' <<'EOF'
+echo 2 >two
+atxec echo 1 @two 3
+>>>
+1 2 3
+EOF
+
+tap3 'file expansion - multiple words' <<'EOF'
+echo "duo deux" >two
+atxec echo 1 @two 3
+>>>
+1 duo deux 3
+EOF
+
+tap3 'file expansion - multiple inserts' <<'EOF'
+echo "duo deux" >two
+atxec echo 1 @two 3 @two
+>>>
+1 duo deux 3 duo deux
+EOF
+
+tap3 'file expansion - multiple words on multiple lines' <<'EOF'
+echo duo >two
+echo deux >>two
+atxec echo 1 @two 3
+>>>
+1 duo deux 3
+EOF
+
+tap3 'file expansion - multiple words on multiple lines, comments' <<'EOF'
+echo duo >two
+echo '# ignored' >>two
+echo deux >>two
+atxec echo 1 @two 3
+>>>
+1 duo deux 3
+EOF
+
+tap3 'file expansion - quoting' <<'EOF'
+echo "'two' 'three'" >two
+atxec echo 1 @two 3
+>>>
+1 two three 3
+EOF
+
+tap3 'file expansion - quoting spaces' <<'EOF'
+echo "'two three'" >two
+atxec printf '%s\n' 1 @two 3
+>>>
+1
+two three
+3
+EOF
+
+tap3 'file expansion - empty file' <<'EOF'
+echo >two
+atxec printf '%s\n' 1 @two 3
+>>>
+1
+3
+EOF
+
+tap3 'file expansion - empty args' <<'EOF'
+echo "''" >two
+atxec printf '%s\n' 1 @two 3
+>>>
+1
+
+3
+EOF
+
+tap3 'file expansion - quoting quote' <<'EOF'
+echo "'quo''te' 'two''quotes''here' 'next''''eachother'" >two
+atxec printf '%s\n' 1 @two 3
+>>>
+1
+quo'te
+two'quotes'here
+next''eachother
+3
+EOF
diff --git a/tap3 b/tap3
new file mode 100755
index 0000000..bfb9bc6
--- /dev/null
+++ b/tap3
@@ -0,0 +1,112 @@
+#!/usr/bin/env perl
+# tap3 [DESC] - check output/error/status of a command against a specification
+#
+# A tiny variant of shelltestrunner (format v1), just takes one test
+# case and outputs a TAP line.
+#
+# Input format:
+#
+# CMD
+# <<<
+# INPUT
+# >>>
+# OUTPUT
+# >>> /OUTPUT REGEX/
+# >>>2
+# STDERR
+# >>>2 /STDERR REGEX/
+# >>>= STATUS
+# >>>= !STATUS
+#
+# All but CMD are optional and can be put in any order,
+# Regex variants can be repeated, all patterns must match.
+# By default, STATUS is set to 0 and STDERR assumed empty.
+#
+# To the extent possible under law, the creator of this work has waived
+# all copyright and related or neighboring rights to this work.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+use strict;
+use warnings;
+use Symbol 'gensym';
+use IPC::Open3;
+
+my $cmd = "";
+my ($input, $output, @output_rx, $stderr, @stderr_rx, $status, $status_not);
+my $ignored = "";
+
+my $var = \$cmd;
+while (<STDIN>) {
+ if (/^#!? /) { next; }
+ if (/^<<<$/) { $var = \$input; $input = ""; next; }
+ if (/^>>>$/) { $var = \$output; $output = ""; next; }
+ if (/^>>>2$/) { $var = \$stderr; $stderr = ""; next; }
+ if (/^>>>\s*\/(.*)\/$/) { push @output_rx, $1; next; }
+ if (/^>>>2\s*\/(.*)\/$/) { push @stderr_rx, $1; next; }
+ if (/^>>>=\s+(\d+)$/) { $var = \$ignored; $status = $1; next; }
+ if (/^>>>=\s+!(\d+)$/) { $var = \$ignored; $status_not = $1; next; }
+ $$var .= $_;
+}
+
+chomp($cmd);
+die "No command to check given\n" if !$cmd;
+
+my ($wtr, $rdr);
+my $err = gensym;
+my $pid = open3($wtr, $rdr, $err, "/bin/sh", "-c", $cmd);
+
+my $desc = shift || $cmd;
+$desc =~ s/\n.*//g;
+
+print $wtr $input if (defined($input));
+close $wtr;
+my $real_output = do { local $/; <$rdr>; };
+my $real_stderr = do { local $/; <$err>; };
+waitpid($pid, 0);
+my $real_status = $? >> 8;
+
+my $r = 0;
+
+sub not_ok {
+ print "not ok - $desc\n" if (!$r);
+ $r = 1;
+ $_[0] =~ s/^/# /mg;
+ print $_[0];
+}
+
+if (defined($output) && $real_output ne $output) {
+ not_ok("wrong output:\n$real_output");
+}
+for my $rx (@output_rx) {
+ if ($real_output !~ $rx) {
+ not_ok("output doesn't match /$rx/:\n$real_output\n");
+ }
+}
+if (defined($stderr) && $real_stderr ne $stderr) {
+ not_ok("wrong stderr:\n$real_stderr");
+}
+for my $rx (@stderr_rx) {
+ if ($real_stderr !~ $rx) {
+ not_ok("stderr doesn't match /$rx/:\n$real_stderr\n");
+ }
+}
+if (!defined($stderr) && !@stderr_rx &&
+ !defined($status) && !defined($status_not) &&
+ $real_stderr) {
+ not_ok("output to stderr:\n$real_stderr\n");
+}
+if (defined($status) && $real_status != $status) {
+ not_ok("wrong status: $real_status (expected $status)\n");
+}
+if (defined($status_not) && $real_status == $status_not) {
+ not_ok("wrong status: $real_status (expected anything else)\n");
+}
+if (!defined($status) && !defined($status_not) &&
+ !defined($stderr) && !@stderr_rx &&
+ $real_status != 0) {
+ not_ok("wrong status: $real_status (command failed)\n");
+}
+
+print "ok - $desc\n" if (!$r);
+
+exit $r;