3 # Copyright (c) 2007, 2008 Andreas Gruenbacher.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions, and the following disclaimer,
11 # without modification, immediately at the beginning of the file.
12 # 2. The name of the author may not be used to endorse or promote products
13 # derived from this software without specific prior written permission.
15 # Alternatively, this software may be distributed under the terms of the
16 # GNU Public License ("GPL").
18 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
22 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33 # Possible improvements:
35 # - distinguish stdout and stderr output
36 # - add environment variable like assignments
37 # - run up to a specific line
38 # - resume at a specific line
44 use POSIX qw(isatty setuid getcwd);
45 use vars qw($opt_l $opt_v);
47 no warnings qw(taint);
49 $opt_l = ~0; # a really huge number
52 my ($OK, $FAILED) = ("ok", "failed");
53 if (isatty(fileno(STDOUT))) {
54 $OK = "\033[32m" . $OK . "\033[m";
55 $FAILED = "\033[31m\033[1m" . $FAILED . "\033[m";
59 sub process_test($$$$);
61 my ($prog, $in, $out) = ([], [], []);
63 my ($tests, $failed) = (0,0);
65 my $width = ($ENV{COLUMNS} || 80) >> 1;
68 my $line = <>; $lineno++;
70 # Substitute %VAR and %{VAR} with environment variables.
71 $line =~ s[%(\w+)][$ENV{$1}]eg;
72 $line =~ s[%\{(\w+)\}][$ENV{$1}]eg;
75 if ($line =~ s/^\s*< ?//) {
77 } elsif ($line =~ s/^\s*> ?//) {
80 process_test($prog, $prog_line, $in, $out);
81 last if $prog_line >= $opt_l;
86 if ($line =~ s/^\s*\$ ?//) {
87 $prog = [ map { s/\\(.)/$1/g; $_ } split /(?<!\\)\s+/, $line ];
93 process_test($prog, $prog_line, $in, $out);
98 my $status = sprintf("%d commands (%d passed, %d failed)",
99 $tests, $tests-$failed, $failed);
100 if (isatty(fileno(STDOUT))) {
102 $status = "\033[31m\033[1m" . $status . "\033[m";
104 $status = "\033[32m" . $status . "\033[m";
108 exit $failed ? 1 : 0;
111 sub process_test($$$$) {
112 my ($prog, $prog_line, $in, $out) = @_;
114 return unless @$prog;
117 print "[$prog_line] \$ ", join(' ',
118 map { s/\s/\\$&/g; $_ } @$p), " -- ";
119 my $result = exec_test($prog, $in);
121 my $nmax = (@$out > @$result) ? @$out : @$result;
122 for (my $n=0; $n < $nmax; $n++) {
124 if (defined $out->[$n] && $out->[$n] =~ /^~ /) {
126 $out->[$n] =~ s/^~ //g;
129 if (!defined($out->[$n]) || !defined($result->[$n]) ||
130 (!$use_re && $result->[$n] ne $out->[$n]) ||
131 ( $use_re && $result->[$n] !~ /^$out->[$n]/)) {
132 push @good, ($use_re ? '!~' : '!=');
135 push @good, ($use_re ? '=~' : '==');
138 my $good = !(grep /!/, @good);
140 $failed++ unless $good;
141 print $good ? $OK : $FAILED, "\n";
142 if (!$good || $opt_v) {
143 for (my $n=0; $n < $nmax; $n++) {
144 my $l = defined($out->[$n]) ? $out->[$n] : "~";
146 my $r = defined($result->[$n]) ? $result->[$n] : "~";
148 print sprintf("%-" . ($width-3) . "s %s %s\n",
160 my ($login, $pass, $uid, $gid) = getpwnam($user)
161 or return [ "su: user $user does not exist\n" ];
163 my $fh = new FileHandle("/etc/group")
164 or return [ "opening /etc/group: $!\n" ];
167 my ($group, $passwd, $gid, $users) = split /:/;
168 foreach my $u (split /,/, $users) {
175 my $groups = join(" ", ($gid, $gid, @groups));
176 #print STDERR "[[$groups]]\n";
177 $! = 0; # reset errno
182 return [ "su: $!\n" ];
188 return [ "su: $prog->[1]: $!\n" ];
191 #print STDERR "[($>,$<)($(,$))]";
199 my $gid = getgrnam($group)
200 or return [ "sg: group $group does not exist\n" ];
201 my %groups = map { $_ eq $gid ? () : ($_ => 1) } (split /\s/, $));
203 #print STDERR "<<", join("/", keys %groups), ">>\n";
204 my $groups = join(" ", ($gid, $gid, keys %groups));
205 #print STDERR "[[$groups]]\n";
206 $! = 0; # reset errno
218 return [ "sg: $!\n" ];
220 print STDERR "[($>,$<)($(,$))]";
226 my ($prog, $in) = @_;
227 local (*IN, *IN_DUP, *IN2, *OUT_DUP, *OUT, *OUT2);
228 my $needs_shell = (join('', @$prog) =~ /[][|<>"'`\$\*\?]/);
230 if ($prog->[0] eq "umask") {
231 umask oct $prog->[1];
233 } elsif ($prog->[0] eq "cd") {
234 if (!chdir $prog->[1]) {
235 return [ "chdir: $prog->[1]: $!\n" ];
239 } elsif ($prog->[0] eq "su") {
240 return su($prog->[1]);
241 } elsif ($prog->[0] eq "sg") {
242 return sg($prog->[1]);
243 } elsif ($prog->[0] eq "export") {
244 my ($name, $value) = split /=/, $prog->[1];
245 # FIXME: need to evaluate $value, so that things like this will work:
246 # export dir=$PWD/dir
247 $ENV{$name} = $value;
249 } elsif ($prog->[0] eq "unset") {
250 delete $ENV{$prog->[1]};
255 or die "Can't create pipe for reading: $!";
256 open *IN_DUP, "<&STDIN"
259 or die "Can't duplicate pipe for reading: $!";
262 open *OUT_DUP, ">&STDOUT"
263 or die "Can't duplicate STDOUT: $!";
265 or die "Can't create pipe for writing: $!";
266 open *STDOUT, ">&OUT2"
267 or die "Can't duplicate pipe for writing: $!";
270 *STDOUT->autoflush();
273 $SIG{CHLD} = 'IGNORE';
278 open *STDIN, "<&IN_DUP"
279 or die "Can't duplicate STDIN: $!";
281 or die "Can't close STDIN duplicate: $!";
283 open *STDOUT, ">&OUT_DUP"
284 or die "Can't duplicate STDOUT: $!";
286 or die "Can't close STDOUT duplicate: $!";
288 foreach my $line (@$in) {
293 or die "Can't close pipe for writing: $!";
299 s#^/bin/sh: line \d+: ##;
308 or die "Can't close read end for input pipe: $!";
310 or die "Can't close write end for output pipe: $!";
312 or die "Can't close STDOUT duplicate: $!";
314 open ERR_DUP, ">&STDERR"
315 or die "Can't duplicate STDERR: $!";
316 open STDERR, ">&STDOUT"
317 or die "Can't join STDOUT and STDERR: $!";
320 exec ('/bin/sh', '-c', join(" ", @$prog));
324 print STDERR $prog->[0], ": $!\n";