From c602bfd08a47f4840bf2e8d96663f76e2fb22f1c Mon Sep 17 00:00:00 2001 From: Leah Neukirchen Date: Wed, 20 Nov 2019 22:15:56 +0100 Subject: initial commit --- Makefile | 26 +++++++++++ README | 47 ++++++++++++++++++++ atxec.1 | 68 +++++++++++++++++++++++++++++ atxec.c | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ t/errors.t | 23 ++++++++++ t/simple.t | 99 ++++++++++++++++++++++++++++++++++++++++++ tap3 | 112 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 519 insertions(+) create mode 100644 Makefile create mode 100644 README create mode 100644 atxec.1 create mode 100755 atxec.c create mode 100644 t/errors.t create mode 100644 t/simple.t create mode 100755 tap3 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 + +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 + * 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 + +#include +#include +#include +#include +#include + +#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 () { + 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; -- cgit 1.4.1