——#!/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;
$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
{
STDERR
"test: $_[0]"
; ++
$ERRORS
; };
$SIG
{__DIE__} =
sub
{
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
">>>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