line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
=head1 NAME |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
Eval::Logic - Evaluate simple logical expressions from a string. |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 DESCRIPTION |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
With this module simple logical expressions from strings which use logical |
8
|
|
|
|
|
|
|
operators like and, or, not and the ternary operator can be evaluated. |
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
This module was created because I wanted to be able to use a simple argument |
11
|
|
|
|
|
|
|
validator which can be fully configured from YAML. This module allows a |
12
|
|
|
|
|
|
|
specification like "we require a_value and some_other_value, or a |
13
|
|
|
|
|
|
|
a_third_option should be specified" to be expressed as a simple string |
14
|
|
|
|
|
|
|
"(a_value && some_other_value) || a_third_option". |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
The module uses eval() and while it does take care to check for anything |
17
|
|
|
|
|
|
|
other than a logical expression you should take a lot of care when |
18
|
|
|
|
|
|
|
evaluating expressions from an untrusted source (in fact, I would not |
19
|
|
|
|
|
|
|
recommend doing that at all). |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
=head1 SYNOPSIS |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
$l = Eval::Logic->new ( '(a || b) && c' ); |
24
|
|
|
|
|
|
|
$l->evaluate ( a => 1, b => 0, c => 1 ); # returns 1 for true |
25
|
|
|
|
|
|
|
$l->evaluate ( a => 1, b => 1, c => 0 ); # returns 0 for false |
26
|
|
|
|
|
|
|
$l->evaluate_if_true ( 'a', 'b' ); # an alternative for that second example |
27
|
|
|
|
|
|
|
$l->evaluate_if_false ( 'c' ); # and another alternative |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
=head1 METHODS |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
=cut |
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
package Eval::Logic; |
34
|
|
|
|
|
|
|
|
35
|
1
|
|
|
1
|
|
540
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
26
|
|
36
|
1
|
|
|
1
|
|
4
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
26
|
|
37
|
|
|
|
|
|
|
|
38
|
1
|
|
|
1
|
|
9
|
use Carp; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
55
|
|
39
|
1
|
|
|
1
|
|
463
|
use Symbol; |
|
1
|
|
|
|
|
767
|
|
|
1
|
|
|
|
|
965
|
|
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
# Forbidden list if truth value names; these are Perl operators with regular |
42
|
|
|
|
|
|
|
# names that cannot be overridden by using 'use subs'. |
43
|
|
|
|
|
|
|
our @forbidden_tv_names = qw( or and not xor ); |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
=head2 new (constructor) |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
$l = Eval::Logic->new ( 'a && b' ); |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
Create a new instance of Eval::Logic. Optionally an expression can be |
50
|
|
|
|
|
|
|
specified which is immediately loaded in the object, see the expression |
51
|
|
|
|
|
|
|
method for more information about the expression syntax. |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
=cut |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
sub new { |
56
|
6
|
|
|
6
|
1
|
3016
|
my $class = shift; |
57
|
6
|
|
|
|
|
20
|
my $self = bless { undef_default => undef }, $class; |
58
|
6
|
50
|
|
|
|
24
|
$self->expression ( @_ ) if ( @_ ); |
59
|
0
|
|
|
|
|
0
|
return $self; |
60
|
|
|
|
|
|
|
} |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
=head2 expression |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
$expression = $l->expression; |
65
|
|
|
|
|
|
|
$l->expression ( 'a && b' ); |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
If called without an argument the current expression is returned, otherwise |
68
|
|
|
|
|
|
|
the current expression in this object is replaced by whatever was specified. |
69
|
|
|
|
|
|
|
If multiple strings are specified they are combined in a single expression |
70
|
|
|
|
|
|
|
that will require all individual expressions to be true. |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
An expression is a string in which the truth values are specified as simple |
73
|
|
|
|
|
|
|
(bare) words which can contain letters, digits and underscores and which |
74
|
|
|
|
|
|
|
must not begin with a digit. In addition to this, the Perl logical |
75
|
|
|
|
|
|
|
operators && (and), || (or), ! (not) can be used, as well as the ternary ?: |
76
|
|
|
|
|
|
|
operator and parentheses. Whitespace is ignored. |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
The barewords TRUE and FALSE have a special meaning which you can probably |
79
|
|
|
|
|
|
|
guess. |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
The method will croak if the expression provided is invalid. |
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
=cut |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
sub expression { |
86
|
6
|
|
|
6
|
1
|
6
|
my $self = shift; |
87
|
6
|
50
|
|
|
|
13
|
if ( @_ ) { |
88
|
|
|
|
|
|
|
|
89
|
6
|
50
|
|
|
|
11
|
my $exp = @_ > 1 ? join ( ' && ', map { '(' . $_ . ')' } @_ ) : $_[0]; |
|
0
|
|
|
|
|
0
|
|
90
|
|
|
|
|
|
|
|
91
|
6
|
|
|
|
|
7
|
my %tv; |
92
|
6
|
|
|
|
|
41
|
foreach my $v ( |
93
|
|
|
|
|
|
|
split / # split on anything that cannot be a truth value: |
94
|
|
|
|
|
|
|
(?: |
95
|
|
|
|
|
|
|
&& | # and operator, |
96
|
|
|
|
|
|
|
\|\| | # or operator, |
97
|
|
|
|
|
|
|
! | # not operator, |
98
|
|
|
|
|
|
|
\? | # the first part of the ternary operator, |
99
|
|
|
|
|
|
|
\: | # the second part of the ternary operator, |
100
|
|
|
|
|
|
|
\( | # opening parentheses, |
101
|
|
|
|
|
|
|
\) | # closing parentheses, |
102
|
|
|
|
|
|
|
\s # any whitespace |
103
|
|
|
|
|
|
|
)+ |
104
|
|
|
|
|
|
|
/x, $exp |
105
|
|
|
|
|
|
|
) { |
106
|
8
|
50
|
|
|
|
14
|
if ( $v ) { |
107
|
8
|
100
|
66
|
|
|
37
|
next if (( $v eq 'TRUE' ) || ( $v eq 'FALSE' )); |
108
|
7
|
100
|
|
|
|
8
|
if ( grep { $v eq $_ } @forbidden_tv_names ) { |
|
28
|
100
|
|
|
|
54
|
|
109
|
2
|
|
|
|
|
186
|
croak "Invalid truth value in expression, named identical to Perl reserved word: '$v'"; |
110
|
|
|
|
|
|
|
} elsif ( $v =~ /^[a-zA-Z_][a-zA-Z_0-9]*$/ ) { |
111
|
1
|
|
|
|
|
3
|
$tv{$v} = undef; |
112
|
|
|
|
|
|
|
} else { |
113
|
4
|
|
|
|
|
495
|
croak "Syntax error or invalid truth value in expression: '$v'"; |
114
|
|
|
|
|
|
|
} |
115
|
|
|
|
|
|
|
} |
116
|
|
|
|
|
|
|
} |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
# Test the expression by evaluating it. |
119
|
0
|
|
|
|
|
|
$self->_eval ( $exp, %tv ); |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
# If we're here, the expression checked out. |
122
|
0
|
|
|
|
|
|
$self->{tv} = [ keys %tv ]; |
123
|
0
|
|
|
|
|
|
$self->{exp} = $exp; |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
} else { |
126
|
0
|
|
|
|
|
|
return $self->{exp}; |
127
|
|
|
|
|
|
|
} |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
=head2 evaluate |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
$outcome = $l->evaluate ( a => 1, b => 0 ); |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
Evaluate the logic expression given the specified truth values. If no |
135
|
|
|
|
|
|
|
default for undefined truth values is specified and some truth values are |
136
|
|
|
|
|
|
|
not defined or not present, a warning is given. |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
The outcome is returned as 1 for true or 0 for false. |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=cut |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
sub evaluate { |
143
|
0
|
|
|
0
|
1
|
|
my $self = shift; |
144
|
0
|
|
|
|
|
|
my %specified_tv = @_; |
145
|
|
|
|
|
|
|
|
146
|
0
|
0
|
0
|
|
|
|
croak 'TRUE or FALSE specified as a variable truth value' if (( exists $specified_tv{TRUE} ) || ( exists $specified_tv{FALSE} )); |
147
|
|
|
|
|
|
|
|
148
|
0
|
0
|
|
|
|
|
if ( defined $self->{exp} ) { |
149
|
0
|
|
|
|
|
|
my %tv; |
150
|
0
|
|
|
|
|
|
foreach my $v ( @{$self->{tv}} ) { |
|
0
|
|
|
|
|
|
|
151
|
0
|
0
|
|
|
|
|
if ( defined $specified_tv{$v} ) { |
|
|
0
|
|
|
|
|
|
152
|
0
|
|
|
|
|
|
$tv{$v} = $specified_tv{$v}; |
153
|
|
|
|
|
|
|
} elsif ( defined $self->{undef_default} ) { |
154
|
0
|
|
|
|
|
|
$tv{$v} = $self->{undef_default}; |
155
|
|
|
|
|
|
|
} else { |
156
|
0
|
0
|
|
|
|
|
carp (( exists $specified_tv{$v} ? 'Undefined' : 'Unspecified' ) . " truth value $v defaults to false" ); |
157
|
0
|
|
|
|
|
|
$tv{$v} = 0; |
158
|
|
|
|
|
|
|
} |
159
|
|
|
|
|
|
|
} |
160
|
0
|
|
|
|
|
|
return $self->_eval ( $self->{exp}, %tv ); |
161
|
|
|
|
|
|
|
} else { |
162
|
0
|
|
|
|
|
|
carp "No expression, returning false"; |
163
|
0
|
|
|
|
|
|
return 0; |
164
|
|
|
|
|
|
|
} |
165
|
|
|
|
|
|
|
} |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
=head2 evaluate_if_false |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
$outcome = $l->evaluate_if_false ( 'a' ); |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
Evaluate the logic expression given the specified values to be false, and |
172
|
|
|
|
|
|
|
all other values to be true. This is a shortcut to the evaluate method. |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=cut |
175
|
|
|
|
|
|
|
|
176
|
0
|
|
|
0
|
1
|
|
sub evaluate_if_false { shift->_eval_if ( 0, @_ ) } |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
=head2 evaluate_if_true |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
$outcome = $l->evaluate_if_true ( 'b' ); |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
Evaluate the logic expression given the specified values to be true, and all |
183
|
|
|
|
|
|
|
other values to be false. This is a shortcut to the evaluate method. |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
=cut |
186
|
|
|
|
|
|
|
|
187
|
0
|
|
|
0
|
1
|
|
sub evaluate_if_true { shift->_eval_if ( 1, @_ ) } |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=head2 truth_values |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
@truth_values = $l->truth_values; |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
Return a list of all variable truth values which are present in the |
194
|
|
|
|
|
|
|
currently loaded expression. |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
=cut |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
sub truth_values { |
199
|
0
|
|
|
0
|
1
|
|
my $self = shift; |
200
|
0
|
0
|
|
|
|
|
if ( defined $self->{exp} ) { |
201
|
0
|
|
|
|
|
|
return @{$self->{tv}}; |
|
0
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
} else { |
203
|
0
|
|
|
|
|
|
carp "No expression, returning empty list"; |
204
|
0
|
|
|
|
|
|
return (); |
205
|
|
|
|
|
|
|
} |
206
|
|
|
|
|
|
|
} |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=head2 undef_default |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
$default = $l->undef_default; |
211
|
|
|
|
|
|
|
$l->undef_default ( $default ); |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
Returns the current default for undefined truth values if specified without |
214
|
|
|
|
|
|
|
an argument, or sets the default value to the specified argument. If you |
215
|
|
|
|
|
|
|
want undefined values to default to false you must explicitly call this |
216
|
|
|
|
|
|
|
method with an argument that is defined and evaluates to false to suppress |
217
|
|
|
|
|
|
|
warnings given about undefined values by the evaluate method. |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
=cut |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
sub undef_default { |
222
|
0
|
|
|
0
|
1
|
|
my $self = shift; |
223
|
0
|
0
|
|
|
|
|
if ( @_ ) { |
224
|
0
|
|
|
|
|
|
$self->{undef_default} = $_[0]; |
225
|
|
|
|
|
|
|
} else { |
226
|
0
|
|
|
|
|
|
return $self->{undef_default}; |
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
} |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
# |
231
|
|
|
|
|
|
|
# The _eval method does the work: it creates a piece of Perl code and then |
232
|
|
|
|
|
|
|
# evaluates it. It will get a bit dirty in here. |
233
|
|
|
|
|
|
|
# |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
sub _eval { |
236
|
0
|
|
|
0
|
|
|
my $self = shift; |
237
|
0
|
|
|
|
|
|
my ( $exp, %tv ) = @_; |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
# Make sure TRUE and FALSE always mean what they say. |
240
|
0
|
|
|
|
|
|
$tv{TRUE} = 1; |
241
|
0
|
|
|
|
|
|
$tv{FALSE} = 0; |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
# Generate a piece of code in a 'scratch' package which we will clean |
244
|
|
|
|
|
|
|
# before using it. |
245
|
0
|
|
|
|
|
|
my $code = ''; |
246
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
# To parse any error messages we count the number of lines added. |
248
|
0
|
|
|
|
|
|
my $our_lines = 0; |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
# Begin with the package declaration and declare the subroutine names |
251
|
|
|
|
|
|
|
# we're using to prevent them from calling core subroutines. |
252
|
0
|
|
|
|
|
|
$code .= 'package ' . __PACKAGE__ . "::Scratch;\n"; $our_lines++; |
|
0
|
|
|
|
|
|
|
253
|
0
|
|
|
|
|
|
$code .= 'use subs qw(' . join ( ' ', keys %tv ) . ");\n"; $our_lines++; |
|
0
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
# Generate a constant subroutine for every value. |
256
|
0
|
|
|
|
|
|
while ( my ( $name, $truth ) = each %tv ) { |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
# For true we use 1, for false we use an empty list because that will |
259
|
|
|
|
|
|
|
# always evaluate to false, even in list context (think about stuff like |
260
|
|
|
|
|
|
|
# '(FALSE)' which must evaluate to false, and not to a list of one |
261
|
|
|
|
|
|
|
# element). |
262
|
|
|
|
|
|
|
|
263
|
0
|
0
|
|
|
|
|
$code .= 'sub ' . $name . '(){' . ( $truth ? '1' : '()' ) . "}\n"; |
264
|
0
|
|
|
|
|
|
$our_lines++; |
265
|
|
|
|
|
|
|
} |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
# Finally we add the expression itself. |
268
|
0
|
|
|
|
|
|
$code .= $exp . "\n;"; |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
# Reset the package namespace and evaluate the generated code block. |
271
|
0
|
|
|
|
|
|
Symbol::delete_package __PACKAGE__ . '::Scratch'; |
272
|
0
|
0
|
|
|
|
|
my $outcome = eval $code ? 1 : 0; |
273
|
|
|
|
|
|
|
|
274
|
0
|
0
|
|
|
|
|
if ( my $error = $@ ) { |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
# Some error messages are changed on the fly to make them clearer... |
277
|
|
|
|
|
|
|
# hopefully. |
278
|
0
|
|
|
|
|
|
$error =~ s/Too many arguments for @{[__PACKAGE__]}::Scratch::(\S+)/Truth value '$1' not followed by boolean operator/; |
|
0
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
# An error occurred while evaluating our code; try to determine the |
281
|
|
|
|
|
|
|
# location of the error. |
282
|
0
|
0
|
|
|
|
|
if ( $error =~ /(at \(eval [0-9]+\) line ([0-9]+))/ ) { |
283
|
0
|
|
|
|
|
|
my ( $location_text, $error_line ) = ( $1, $2 ); |
284
|
0
|
|
|
|
|
|
$error_line -= $our_lines; |
285
|
0
|
0
|
|
|
|
|
if ( $error_line > 0 ) { # the error was in the expression, change the error message to be more descriptive |
286
|
0
|
|
|
|
|
|
$error =~ s/\Q$location_text\E/at line $error_line in logical expression/; |
287
|
0
|
|
|
|
|
|
croak $error; |
288
|
|
|
|
|
|
|
} else { # woops |
289
|
0
|
|
|
|
|
|
croak "Eval::Logic internal error while evaluating expression: $error"; |
290
|
|
|
|
|
|
|
} |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
# If we're still here we just repeat whatever error we got. |
294
|
0
|
|
|
|
|
|
croak $error; |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
} |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
# Make sure we always return 1 for true and 0 for false. |
299
|
0
|
0
|
|
|
|
|
return $outcome ? 1 : 0; |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
} |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
# |
304
|
|
|
|
|
|
|
# General implementation of evaluate_if_(true|false) |
305
|
|
|
|
|
|
|
# |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
sub _eval_if { |
308
|
0
|
|
|
0
|
|
|
my $self = shift; |
309
|
0
|
|
|
|
|
|
my $truth = shift; |
310
|
0
|
|
|
|
|
|
my @values = @_; |
311
|
0
|
0
|
|
|
|
|
my %tv = map { $_ => $truth ? 0 : 1 } @{$self->{tv}}; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
312
|
0
|
|
|
|
|
|
foreach ( @values) { $tv{$_} = $truth } |
|
0
|
|
|
|
|
|
|
313
|
0
|
|
|
|
|
|
return $self->evaluate ( %tv ); |
314
|
|
|
|
|
|
|
} |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=head1 AUTHOR |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
Sebastiaan Hoogeveen |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=head1 COPYRIGHT |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
Copyright (c) 2016 Sebastiaan Hoogeveen. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
See http://www.perl.com/perl/misc/Artistic.html |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
=cut |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
1; |