about summary refs log tree commit diff
diff options
context:
space:
mode:
-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;