#!/usr/bin/perl =begin metadata Name: test Description: condition evaluation utility Author: Brad Appleton License: perl =end metadata =cut ########################################################################### # test -- Perl script to emulate BSD Unix test(1) functionality # (with slightly more informative error messages). # # Copyright (c) 1999 Brad Appleton. All rights reserved. # # This file is part of "Perl Power Tools: the Unix Reconstruction # Project" (PPT) which is free software. You can redistribute it # and/or modify it under the same terms as PPT itself. # ########################################################################### package PerlPowerTools::test; use strict; #use diagnostics; use vars qw($VERSION); $VERSION = 1.01; ## Set to 0 to disable debug output; non-zero to enable it my $DEBUG = $ENV{DEBUG_PPT_TEST} || 0; sub dbval($) { local $_ = shift; return '<undef>' unless defined; return ($_ ? "true" : "false") if /^\d+$/; return (/^-\w/ ? $_ : "'$_'"); } ## Keep track of errors/warnings my $ERRORS = 0; $SIG{__WARN__} = sub { print STDERR "test: $_[0]"; ++$ERRORS; }; $SIG{__DIE__} = sub { print STDERR "test: $_[0]"; exit 2; }; ## Complain about an invalid argument sub bad_arg(@) { warn "invalid argument @_\n"; } ## Make sure we have a numeric value sub number($) { local $_ = shift; return $_ if (defined && /^[-+]?\d+$/); bad_arg "'$_[0]' - expecting a number"; return undef; } ## %TEST_OPS maps test(1) operators or operations to Perl operations ## If a value is a string, the string is returned. If it is a reference, ## then its assumed to be a reference to a subroutine, and the value ## is whatever the subroutine returns. ## ## The code doing the lookup is responsible for passing the correct ## number of parameters and interpreting the result. ## my %TEST_OPS = ( ## Logical/grouping operators ('-a' has higher precedence than '-o') '(' => '(', ')' => ')', '!' => '!', '-a' => '&&', '-o' => '||', ## NOTE: are && and || 'compatibility' or feature creep? '&&' => '&&', '||' => '||', ## File test operators '-b' => sub { -b $_[0] }, '-c' => sub { -c $_[0] }, '-d' => sub { -d $_[0] }, '-e' => sub { -e $_[0] }, '-f' => sub { -f $_[0] }, '-g' => sub { -g $_[0] }, '-h' => sub { -l $_[0] }, '-k' => sub { -k $_[0] }, '-l' => sub { -l $_[0] }, '-p' => sub { -p $_[0] }, '-r' => sub { -r $_[0] }, '-s' => sub { -s $_[0] }, '-t' => sub { -t (number($_[0])||0) }, '-u' => sub { -u $_[0] }, '-w' => sub { -w $_[0] }, '-x' => sub { -x $_[0] }, '-B' => sub { -B $_[0] }, '-L' => sub { -l $_[0] }, '-O' => sub { -O $_[0] }, '-G' => sub { bad_arg("'-G' - operator not supported") }, '-R' => sub { -R $_[0] }, '-S' => sub { -S $_[0] }, '-T' => sub { -T $_[0] }, '-W' => sub { -W $_[0] }, '-X' => sub { -X $_[0] }, ## String comparisons '-n' => sub { length $_[0] }, '-z' => sub { ! length $_[0] }, '=' => sub { $_[0] eq $_[1] }, '!=' => sub { $_[0] ne $_[1] }, '<' => sub { $_[0] lt $_[1] }, '>' => sub { $_[0] gt $_[1] }, ## NOTE: are ==, <=, and >= 'compatibility' or feature creep? '==' => sub { $_[0] eq $_[1] }, '<=' => sub { $_[0] le $_[1] }, '>=' => sub { $_[0] ge $_[1] }, ## Numeric comparisons '-eq' => sub { (number($_[0])||0) == (number($_[1])||0) }, '-ne' => sub { (number($_[0])||0) != (number($_[1])||0) }, '-lt' => sub { (number($_[0])||0) < (number($_[1])||0) }, '-le' => sub { (number($_[0])||0) <= (number($_[1])||0) }, '-gt' => sub { (number($_[0])||0) > (number($_[1])||0) }, '-ge' => sub { (number($_[0])||0) >= (number($_[1])||0) }, ## File comparisons '-nt' => sub { -M $_[0] < -M $_[1] }, '-ot' => sub { -M $_[0] > -M $_[1] }, '-ef' => sub { bad_arg("'-ef' - operator not supported") }, ); ## Apply a test operator to the given arguments sub apply_op ($;@) { my $test_op = shift; my $perl_op = $TEST_OPS{$test_op}; my $result = undef; if (defined $perl_op) { $result = (ref $perl_op) ? 0+&{$perl_op}(@_) : $perl_op; } else { ## NOTE: should this be an error, or should it be the ## same as saying "-n $test_op"? warn "invalid operator '$test_op'\n"; } return $result; } ## Evaluate test(1) operations and their operands. ## Returns 'undef' upon error, 0 false, !0 for true sub test (@) { my @terms = (); local $_ = ""; my $grouping = 0; my $need_expr = 1; while (@_ > 0) { $_ = shift; next if m/[\[;\]]/; ## ignore '[', ']', and ';' if (/^[\(\)!]$/) { ## grouping and negation if ($_ eq ')') { --$grouping; bad_arg "'$_' - unbalanced parentheses" if ($grouping < 0); push @terms, 0 if ($need_expr); $need_expr = 0; } else { ++$grouping if ($_ eq '('); bad_arg "'$_' - expecting '-a' or '-o'" unless ($need_expr); } $DEBUG and printf ">>> %s", $_; push @terms, apply_op($_); } elsif (/^(?: -[ao] | \|\| | \&\& )$/x) { ## and/or operators bad_arg "'$_' - expression expected" if ($need_expr); $DEBUG and printf ">>> %s", $_; push @terms, apply_op($_); $need_expr = 1; } elsif ($_ eq '-t' and (@_ == 0 or !defined(number $_[0]))) { ## '-t' with no argument bad_arg "'$_' - expecting '-a' or '-o'" unless ($need_expr); $DEBUG and printf ">>> %s", $_; push @terms, 0+(-t ); $need_expr = 0; } elsif (/^-\w$/) { ## file tests and string length/existence operators bad_arg "'$_' - expecting '-a' or '-o'" unless ($need_expr); warn "argument expected after '$_'\n" if (@_ == 0); if (@_ > 0) { $DEBUG and printf ">>> %s %s", $_, $_[0]; push @terms, apply_op($_, shift); $need_expr = 0; } } elsif (@_ and $_[0] =~ /^(?: -[a-z]{2} | [=!]?= | [<>]=? )$/x) { ## We have a string or a filename that participates ## in a binary infix operation. bad_arg "'$_' - expecting '-a' or '-o'" unless ($need_expr); warn "argument expected after '${\shift()}'\n" if (@_ < 2); if (@_ >= 2) { $DEBUG and printf ">>> %s %s %s", $_, @_[0..1]; push @terms, apply_op(shift, $_, shift); $need_expr = 0; } } else { ## We have a lone string, so "-n string" is implied. bad_arg "'$_' - expecting '-a' or '-o'" unless ($need_expr); $DEBUG and printf ">>> %s", dbval $_; push @terms, length; $need_expr = 0; } $DEBUG and @terms and printf " ==> %s\n", dbval($terms[-1]); } ## Check for errors so far warn "too many open '(' parentheses\n" if ($grouping > 0); return undef if $ERRORS; ## Now we have all our primitive terms evaluated, compute the result. $DEBUG and print ">>>expression is: @terms\n"; return $terms[0] if (@terms == 1 and $terms[0] =~ /^\d*$/); my $result = eval "@terms"; warn "$@" if ($@ and !$ERRORS); return ($@) ? undef : 0+$result; } exit(1) if (@ARGV == 0); my $rc = test @ARGV; ## For command-lines, zero is success and non-zero is false, so ## we need to interpret the return code into an exit status. exit(defined($rc) ? !$rc : 2); __END__ =head1 NAME test - condition evaluation utility =head1 SYNOPSIS test expression [ expression ] =head1 DESCRIPTION The B<test> utility evaluates the I<expression> and, if it evaluates to true, returns a zero (true) exit status; otherwise it returns 1 (false). If no expression is given, B<test> also returns 1 (false). All operators and flags are separate arguments to the test utility. The following primaries are used to construct expression: =over 14 =item B<-b> I<file> True if file exists and is a block special file. =item B<-c> I<file> True if file exists and is a character special file. =item B<-d> I<file> True if file exists and is a directory. =item B<-e> I<file> True if file exists (regardless of type). =item B<-f> I<file> True if file exists and is a regular file. =item B<-g> I<file> True if file exists and its set-group-ID flag is set. =item B<-h> I<file> True if file exists and is a symbolic link. This operator is for COMPATABILITY purposes, do not rely on its existence. Use B<-L> instead. =item B<-k> I<file> True if file exists and its sticky bit is set. =item B<-n> I<string> True if the length of string is nonzero. =item B<-p> I<file> True if file is a named pipe (FIFO). =item B<-r> I<file> True if file exists and is readable by the effective user/group. =item B<-s> I<file> True if file exists and has a size greater than zero. =item B<-t> I<file_descriptor> True if the file whose file descriptor number is I<file_descriptor> is open and is associated with a terminal. =item B<-t> Same as above with an implicit file descriptor number of "1" (e.g.: B<-t> 1). =item B<-u> I<file> True if file exists and its set-user-ID flag is set. =item B<-w> I<file> True if file exists and is writable by the effective user/group. True indicates only that the write flag is on. The file is not writable on a read-only file system even if this test indicates true. =item B<-x> I<file> True if file exists and is executable by the effective user/group. True indicates only that the execute flag is on. If file is a directory, true indicates that file can be searched. =item B<-z> I<string> True if the length of string is zero. =item B<-B> I<file> True if file exists and is a binary file. =item B<-L> I<file> True if file exists and is a symbolic link. =item B<-O> I<file> True if file exists and its owner matches the effective user ID of this process. =begin _NOT_IMPLEMENTED_ =item B<-G> I<file> True if file exists and its group matches the effective group ID of this process. =end _NOT_IMPLEMENTED_ =item B<-R> I<file> True if file exists and is readable by the real user/group. =item B<-S> I<file> True if file exists and is a socket. =item B<-T> I<file> True if file exists and is a text file. =item B<-W> I<file> True if file exists and is writable by the real user/group. True indicates only that the write flag is on. The file is not writable on a read-only file system even if this test indicates true. =item B<-X> I<file> True if file exists and is executable by the real user/group. True indicates only that the execute flag is on. If file is a directory, true indicates that file can be searched. =item I<file1> B<-nt> I<file2> True if I<file1> exists and is newer than I<file2>. =item I<file1> B<-ot> I<file2> True if I<file1> exists and is older than I<file2>. =begin _NOT_IMPLEMENTED_ =item I<file1> B<-ef> I<file2> True if I<file1> and I<file2> exist and refer to the same file. =end _NOT_IMPLEMENTED_ =item I<string> True if I<string> is not the null string. =item I<s1> = I<s2> True if the strings I<s1> and I<s2> are identical. =item I<s1> != I<s2> True if the strings I<s1> and I<s2> are not identical. =item I<s1> < I<s2> True if string I<s1> comes before I<s2> based on the ASCII value of their characters. =item I<s1> > I<s2> True if string I<s1> comes after I<s2> based on the ASCII value of their characters. =item I<s1> True if I<s1> is not the null string. =item I<n1> B<-eq> I<n2> True if the integers I<n1> and I<n2> are algebraically equal. =item I<n1> B<-ne> I<n2> True if the integers I<n1> and I<n2> are not algebraically equal. =item I<n1> B<-gt> I<n2> True if the integer I<n1> is algebraically greater than the integer I<n2>. =item I<n1> B<-ge> I<n2> True if the integer I<n1> is algebraically greater than or equal to the integer I<n2>. =item I<n1> B<-lt> I<n2> True if the integer I<n1> is algebraically less than the integer I<n2>. =item I<n1> B<-le> I<n2> True if the integer I<n1> is algebraically less than or equal to the integer I<n2>. =back These primaries can be combined with the following operators: =over 14 =item ! I<expression> True if I<expression> is false. =item I<expression1> B<-a> I<expression2> True if both I<expression1> and I<expression2> are true. =item I<expression1> B<-o> I<expression2> True if either I<expression1> or I<expression2> are true. =item (I<expression>) True if I<expression> is true (uses parentheses for grouping). =back Note that the B<-a> operator has higher precedence than the B<-o> operator. Notice also that all the operators and flags are separate arguments to test. =head1 RETURN VALUES The test utility exits with one of the following values: =over 8 =item $return_val == 0 Expression evaluated to true. =item $return_val == 1 Expression evaluated to false or expression was missing. =item $return_val > 1 An error occurred. =back =head1 CAVEATS Command line arguments like parentheses and arithmetic operators (e.g.: '(', ')', '!', '>', '<', etc.) I<may> be meaningful to the command-line interpreter (shell) and therefore I<may> need to be escaped from any special shell interpretation. =head1 SEE ALSO L<sh>, L<find> =cut