File Coverage

blib/lib/Lab/SCPI.pm
Criterion Covered Total %
statement 257 308 83.4
branch 120 168 71.4
condition 6 9 66.6
subroutine 19 19 100.0
pod 6 9 66.6
total 408 513 79.5


line stmt bran cond sub pod time code
1             package Lab::SCPI;
2             $Lab::SCPI::VERSION = '3.901';
3             #ABSTRACT: Match L headers and parameters against keywords
4              
5 5     5   74589 use v5.20;
  5         28  
6              
7 5     5   29 use warnings;
  5         9  
  5         154  
8 5     5   23 no warnings 'recursion';
  5         8  
  5         197  
9 5     5   37 use strict;
  5         10  
  5         125  
10              
11 5     5   26 use Carp;
  5         8  
  5         305  
12 5     5   2288 use English; # avoid editor nonsense with odd special variables
  5         9773  
  5         27  
13 5     5   2045 use Exporter 'import';
  5         10  
  5         17068  
14              
15             our @EXPORT = qw( scpi_match scpi_parse scpi_canon
16             scpi_flat scpi_parse_sequence );
17              
18             our $WS = qr/[\x00-\x09\x0b-\x20]/; # whitespace std488-2 7.4.1.2
19              
20              
21             sub scpi_match {
22 24     24 1 14098 my $header = shift;
23 24         40 my $keyword = shift;
24 24         71 my @keywords = split '\|', $keyword, -1;
25 24         48 for my $part (@keywords) {
26 27 100       55 if ( match_keyword( $header, $part ) ) {
27 13         58 return 1;
28             }
29             }
30 11         42 return 0;
31             }
32              
33             sub parse_keyword {
34 53     53 0 93 my $keyword = shift;
35              
36             # For the first part, the colon is optional.
37 53         171 my $start_mnemonic_regex = qr/(?:?[a-z][a-z0-9_]*)/i;
38 53         128 my $mnemonic_regex = qr/(?:[a-z][a-z0-9_]*)/i;
39 53         253 my $keyword_regex = qr/\[$mnemonic_regex\]|$mnemonic_regex/;
40 53         360 my $start_regex = qr/\[$start_mnemonic_regex\]|$start_mnemonic_regex/;
41              
42             # check if keyword is valid
43 53 50       123 if ( length($keyword) == 0 ) {
44 0         0 croak "keyword with empty length";
45             }
46              
47 53 50       409 if ( $keyword !~ /^${start_regex}${keyword_regex}*$/ ) {
48 0         0 croak "invalid keyword: '$keyword'";
49             }
50              
51 53 100       289 if ( $keyword !~ /\[/ ) {
52              
53             # no more optional parts
54 40         224 return $keyword;
55             }
56              
57             #recurse
58             return (
59 13         70 parse_keyword( $keyword =~ s/\[(.*?)\]/$1/r ),
60             parse_keyword( $keyword =~ s/\[(.*?)\]//r )
61             );
62             }
63              
64              
65             sub scpi_shortform {
66 55     55 1 469 my $string = shift;
67 55         273 $string =~ s/^${WS}*//; # strip leading spaces
68 55 100       133 if ( length($string) <= 4 ) {
69 10         32 return $string;
70             }
71              
72             # common mnemonics start with '*' and are not shortenable
73             # note that standard IEEE488 common mnemonics are length 4,
74             # but some extensions result in longer common mnemonics
75              
76 45 50       121 if ( $string =~ /^\*/ ) {
77 0         0 return $string;
78             }
79              
80             # mnemonics can have following digits (ex: CHANNEL3)
81             # the digits should be kept
82             # if followed by a '?', keep that too
83              
84             # mnemonics in the form (letter)(letter|digit|underscore)*
85             # but need to separate the "digit" part at end
86              
87 45 50       146 if ( $string =~ /^([a-z]\w*[a-z_])(\d*)(\??)/i ) {
88 45         110 $string = substr( $1, 0, 4 );
89 45         84 my $n = $2;
90 45         107 my $q = $3;
91 45 100       118 if ( $string =~ /^...[aeiou]/i ) {
92 8         28 $string = substr( $string, 0, 3 );
93             }
94 45         183 return $string . $n . $q;
95             }
96             else { # not a standard form mnemonic, bail
97 0         0 return $string;
98             }
99              
100             }
101              
102             # Return 1 for equal, 0 if not.
103             sub compare_headers {
104 37     37 0 53 my $a = shift;
105 37         60 my $b = shift;
106              
107 37         79 my @a = split( /:/, $a, -1 );
108 37         66 my @b = split( /:/, $b, -1 );
109              
110 37 100       76 if ( @a != @b ) {
111 11         35 return 0;
112             }
113 26         54 while (@a) {
114 34         61 my $a = shift @a;
115 34         46 my $b = shift @b;
116 34         58 $a = "\L$a";
117 34         48 $b = "\L$b";
118 34 100 100     94 if ( $b ne $a and $b ne scpi_shortform($a) ) {
119 13         44 return 0;
120             }
121             }
122 13         29 return 1;
123             }
124              
125             # Return 1 for match, 0 for no match.
126             sub match_keyword {
127 27     27 0 47 my $header = shift;
128 27         35 my $keyword = shift;
129              
130             # strip leading and trailing whitespace
131 27         126 $header =~ s/^\s*//;
132 27         112 $header =~ s/\s*$//;
133              
134 27         60 my @combinations = parse_keyword($keyword);
135 27         53 for my $combination (@combinations) {
136 37 100       68 if ( compare_headers( $combination, $header ) ) {
137 13         41 return 1;
138             }
139             }
140 14         36 return 0;
141             }
142              
143              
144             sub scpi_parse {
145 4     4 1 12736 my $str = shift;
146 4         9 my $d = shift;
147 4 50       16 $d = {} unless defined($d);
148 4         14 _gMem( $str, 0, $d, $d );
149 4         44 return $d;
150             }
151              
152             # "get Mnemonic"
153             # recursive parse _gMem(string,level,treetop,treebranch)
154             # level = 0 is the top of the tree, descend as elements
155             # of the scpi command are parsed: :lev0:lev1:lev2;lev2;lev2:lev3;lev3 ...
156              
157             sub _gMem {
158 105     105   213 my $str = shift;
159 105         136 my $level = shift;
160 105         125 my $dtop = shift;
161 105         128 my $d = shift;
162              
163 105 50       511 if ( $str =~ /^${WS}*(;|\s*$)/ ) {
164 0         0 return '';
165             }
166              
167 105         152 while (1) {
168 284         938 $str =~ s/^${WS}*//;
169 284 100       758 last if $str =~ /^\s*$/;
170              
171 182 100       386 if ( $level == 0 ) {
172 35 50       238 if ( $str =~ /^(\*\w+\??)${WS}*(;|\s*$)/i ) { #common
    100          
    100          
173 0 0       0 $dtop->{$1} = {} unless exists $dtop->{$1};
174 0         0 $str = $POSTMATCH;
175 0         0 next;
176             }
177             elsif ( $str =~ /^(\*\w+\??)${WS}+/i ) { # common with params
178 1 50       6 $dtop->{$1} = {} unless exists $dtop->{$1};
179 1         4 $str = _scpi_value( $POSTMATCH, $dtop->{$1} );
180 1 50       47 if ( $str =~ /^${WS}*(;|\s*$)/ ) {
181 1         3 $str = $POSTMATCH;
182 1         5 next;
183             }
184             else {
185 0         0 croak("parse error after common command");
186             }
187             }
188             elsif ( $str =~ /^:/ ) { # leading :
189 26         40 $d = $dtop;
190 26         74 $str =~ s/^://;
191             }
192             }
193             else {
194 147 50       255 if ( $str =~ /^\*/ ) {
195 0         0 croak("common command on level>0");
196             }
197 147 50       263 if ( $str =~ /^:/ ) {
198 0         0 croak("leading : on level > 0");
199             }
200             }
201              
202 181         626 $str =~ s/^${WS}*//;
203 181 50       473 last if $str =~ /^\s*$/;
204              
205 181 100       351 if ( $str =~ /^;/ ) { # another branch, same or top level
206 77         289 $str =~ s/^;${WS}*//;
207 77 100       208 last if $str =~ /^\s*$/;
208 74         104 my $nlev = $level;
209 74 100       146 $nlev = 0 if $str =~ /^[\*\:]/;
210              
211             # print "level=$level nlev=$nlev str=$str\n";
212 74         248 $str = _gMem( $str, $nlev, $dtop, $d );
213 74         117 next;
214             }
215              
216 104 50       1162 if ( $str =~ /^(\w+\??)${WS}*(;|\s*$)/i ) { # leaf, no params
    100          
    50          
217 0 0       0 $d->{$1} = {} unless exists $d->{$1};
218 0         0 return $POSTMATCH;
219             }
220             elsif ( $str =~ /^(\w+)${WS}*:/i ) { # branch, go down a level
221 27 100       124 $d->{$1} = {} unless exists $d->{$1};
222 27         143 $str = _gMem( $POSTMATCH, $level + 1, $dtop, $d->{$1} );
223             }
224             elsif ( $str =~ /^(\w+\??)${WS}+/i ) { # leaf with params
225 77 50       327 $d->{$1} = {} unless exists $d->{$1};
226 77         168 $str = $POSTMATCH;
227 77         167 $str = _scpi_value( $str, $d->{$1} );
228             }
229             else {
230 0         0 croak("parse error on '$str'");
231             }
232             }
233 105         217 return $str;
234             }
235              
236             sub _scpi_value {
237 154     154   298 my $str = shift;
238 154         195 my $d = shift;
239              
240 154         287 $d->{_VALUE} = '';
241 154         189 my $lastsp = 0;
242 154         633 while ( $str !~ /^${WS}*$/ ) {
243 325         1873 $str =~ s/^${WS}*//;
244              
245 325 100       2122 if ( $str =~ /^;/ ) {
    100          
    50          
    100          
    50          
    50          
246 154 100       605 $d->{_VALUE} =~ s/\s*$// if $lastsp;
247              
248 154         460 return $str;
249             }
250             elsif ( $str =~ /^\#([1-9])/ ) { # counted arbitrary
251 1         2 my $nnd = $1;
252 1         4 my $nd = substr( $str, 2, $nnd );
253 1         5 $d->{_VALUE} .= substr( $str, 0, $nd + 2 + $nnd );
254 1 50       5 if ( length($str) > $nd + 2 + $nnd ) {
255 1         4 $str = substr( $str, $nd + 2 + $nnd );
256             }
257             else {
258 0         0 $str = '';
259             }
260 1         2 $lastsp = 0;
261             }
262             elsif ( $str =~ /^\#0/ ) { #uncounted arbitrary
263 0         0 $d->{_VALUE} .= $str;
264 0         0 $str = '';
265 0         0 return $str;
266             }
267             elsif ( $str =~ /^(\"(?:([^\"]+|\"\")*)\")${WS}*/ )
268             { # double q string
269 10         33 $d->{_VALUE} .= $1 . ' ';
270 10         21 $str = $POSTMATCH;
271 10         16 $lastsp = 1;
272             }
273             elsif ( $str =~ /^(\'(?:([^\']+|\'\')*)\')${WS}*/ )
274             { # single q string
275 0         0 $d->{_VALUE} .= $1 . ' ';
276 0         0 $str = $POSTMATCH;
277 0         0 $lastsp = 1;
278             }
279             elsif ( $str =~ /^([\w\-\+\.\%\!\#\~\=\*]+)${WS}*/i )
280             { #words, numbers
281 160         433 $d->{_VALUE} .= $1 . ' ';
282 160         300 $str = $POSTMATCH;
283 160         212 $lastsp = 1;
284             }
285             else {
286 0         0 croak("parse error, parameter not matched with '$str'");
287             }
288 171 100       988 if ( $str =~ /^${WS}*,/ ) { #parameter separator
289 14         32 $str = $POSTMATCH;
290 14 50       128 $d->{_VALUE} =~ s/${WS}*$// if $lastsp;
291 14         33 $d->{_VALUE} .= ',';
292 14         56 $lastsp = 0;
293             }
294             }
295 0 0       0 $d->{_VALUE} =~ s/\s*$// if $lastsp;
296 0         0 return $str;
297             }
298              
299              
300             sub scpi_parse_sequence {
301 3     3 1 29142 my $str = shift;
302 3         7 my $d = shift;
303 3 50       12 $d = [] unless defined($d);
304              
305 3         36 $str =~ s/^${WS}+//;
306 3 50       15 $str = ':' . $str unless $str =~ /^[\*:]/;
307 3 100       14 $str = $str . ';' unless $str =~ /;$/; # :string; form
308              
309 3         7 my (@cur) = ();
310 3         6 my $level = 0;
311              
312 3         5 while (1) {
313 91         261 $str =~ s/^${WS}+//;
314 91 100       238 if ( $str =~ /^;/ ) {
315 79         269 $str =~ s/^;${WS}*//;
316 79         146 my $ttop = {};
317 79         98 my $t = $ttop;
318              
319 79         164 for ( my $j = 0; $j <= $#cur; $j++ ) {
320 241         355 my $k = $cur[$j];
321 241 100       369 if ( $k eq '_VALUE' ) {
322 76         156 $t->{$k} = $cur[ $j + 1 ];
323 76         110 last;
324             }
325             else {
326 165         344 $t->{$k} = undef;
327 165 100       337 $t->{$k} = {} if $j < $#cur;
328 165         348 $t = $t->{$k};
329             }
330             }
331 79         107 push( @{$d}, $ttop );
  79         156  
332             }
333              
334 91 100       257 last if $str =~ /^\s*;?\s*$/; # handle trailing newline too
335              
336             # print "lev=$level str='$str'\n";
337 88 100       170 if ( $level == 0 ) {
338              
339             # starting from prev command
340 79 100       166 if ( $str =~ /^\w/i ) { # prev A:b or A:b:_VALUE:v
341 52         71 pop(@cur);
342 52         76 my $v = pop(@cur);
343 52 50 33     161 if ( defined($v) && $v eq '_VALUE' ) {
344 52         67 pop(@cur);
345             }
346             else {
347 0 0       0 push( @cur, $v ) if defined($v);
348             }
349 52         82 $level = 1;
350              
351             }
352             else {
353 27 100       62 if ( $str =~ /^:/ ) {
354 26         118 $str =~ s/^:${WS}*//;
355             }
356 27 50       80 next if $str =~ /^\s*;?\s*$/;
357 27         51 @cur = ();
358 27 100       369 if ( $str =~ /^(\*\w+\??)${WS}*;/i ) {
    50          
    100          
    50          
    50          
359              
360             # common, no arg
361 1         3 push( @cur, $1 );
362 1         5 $str = ';' . $POSTMATCH;
363              
364             }
365             elsif ( $str =~ /^(\*\w+\??)${WS}+/i ) {
366              
367             # common, arguments
368 0         0 push( @cur, $1 );
369 0         0 my $tmp = {};
370 0         0 $str = _scpi_value( $POSTMATCH, $tmp );
371 0         0 push( @cur, '_VALUE' );
372 0         0 push( @cur, $tmp->{_VALUE} );
373              
374             }
375             elsif ( $str =~ /^(\w+)${WS}*:/i ) {
376              
377             # start of tree, more coming
378 19         49 push( @cur, $1 );
379 19         36 $str = $POSTMATCH;
380 19         35 $level = 1;
381              
382             }
383             elsif ( $str =~ /^(\w+\??)${WS}*;/i ) {
384              
385             # tree end
386 0         0 push( @cur, "$1" );
387 0         0 $str = ';' . $POSTMATCH;
388              
389             }
390             elsif ( $str =~ /^(\w+\??)${WS}*/i ) {
391              
392             # tree end, args
393 7         21 push( @cur, $1 );
394 7         11 my $tmp = {};
395 7         18 $str = _scpi_value( $POSTMATCH, $tmp );
396 7         14 push( @cur, '_VALUE' );
397 7         17 push( @cur, $tmp->{_VALUE} );
398              
399             }
400             else {
401 0         0 croak("parse error str='$str'");
402             }
403             }
404              
405             }
406 88         248 $str =~ s/^${WS}+//;
407 88 50       221 next if $str =~ /^\s*;?\s*$/;
408              
409 88 100       167 if ( $level > 0 ) { # level > 0
410 80 50       144 if ( $str =~ /^[\*:]/ ) {
411 0         0 croak("common|root at level > 0");
412             }
413 80 100       676 if ( $str =~ /^(\w+)${WS}*:/i ) { #down another level
    100          
    50          
414 9         20 push( @cur, $1 );
415 9         20 $str = $POSTMATCH;
416              
417             # $level++;
418              
419             }
420             elsif ( $str =~ /^(\w+\??)${WS}*;/i ) { # end tree
421 2         4 push( @cur, $1 );
422 2         5 $str = ';' . $POSTMATCH;
423 2         5 $level = 0;
424              
425             }
426             elsif ( $str =~ /^(\w+\??)${WS}+/i ) { #arguments
427              
428 69         172 push( @cur, $1 );
429 69         110 my $tmp = {};
430 69         131 $str = _scpi_value( $POSTMATCH, $tmp );
431 69         127 push( @cur, '_VALUE' );
432 69         134 push( @cur, $tmp->{_VALUE} );
433 69         175 $level = 0;
434              
435             }
436             else {
437 0         0 croak("parse error str='$str'");
438             }
439             }
440              
441             }
442              
443 3         53 return $d;
444             }
445              
446              
447             sub scpi_canon {
448 98     98 1 183 my $h = shift;
449 98         118 my $override = shift;
450 98         119 my $top = shift;
451 98 100       185 $override = {} unless defined $override;
452 98 100       156 $top = 1 unless defined $top;
453 98         139 my $n = {};
454 98         126 my $s;
455              
456 98         116 foreach my $k ( keys( %{$h} ) ) {
  98         226  
457              
458 119 100       204 if ( $k eq '_VALUE' ) {
459 44         97 $n->{$k} = $h->{$k};
460             }
461             else {
462 75 100       127 if ($top) {
463 29 50       61 if ( $k =~ /^(\*\w+\??)/i ) { #common
464 0         0 $n->{ uc($1) } = undef;
465 0 0       0 if ( defined( $h->{$k} ) ) {
466 0         0 croak("common command with subcommand");
467             }
468 0         0 next;
469             }
470             }
471              
472 75 50       427 if ( $k =~ /^([a-z]\w*[a-z_])${WS}*(\d*)(\??)/i ) {
473 75         157 my $m = $1;
474 75         115 my $num = $2;
475 75 50       132 $num = '' unless defined $num;
476 75         107 my $q = $3;
477 75 50       120 $q = '' unless defined $q;
478              
479 75         95 my $ov = 0;
480 75         88 foreach my $ko ( keys( %{$override} ) ) {
  75         164  
481 220         313 my $shorter = $ko;
482 220         567 $shorter =~ s/[a-z]\w*$//;
483 220 100 66     714 if ( uc($ko) eq uc($m) || $shorter eq uc($m) ) {
484 49         74 $m = $shorter;
485 49         71 $s = "$m$num$q";
486             $n->{$s}
487 49         106 = scpi_canon( $h->{$k}, $override->{$ko}, 0 );
488 49         65 $ov = 1;
489 49         64 last;
490             }
491             }
492 75 100       170 next if $ov;
493              
494 26         43 $s = uc( scpi_shortform($m) ) . $num . $q;
495             $n->{$s}
496 26         67 = scpi_canon( $h->{$k}, {}, 0 ); # no override lower too
497             }
498             else {
499 0         0 croak("parse error, mnemonic '$k'");
500             }
501              
502             }
503              
504             }
505 98         267 return $n;
506             }
507              
508              
509             sub scpi_flat {
510 23     23 1 145 my $h = shift;
511 23         30 my $ov = shift;
512              
513 23 50       47 if ( ref($h) eq 'HASH' ) {
    0          
514 23         35 my $f = {};
515 23         45 my $c = scpi_canon( $h, $ov );
516 23         51 _scpi_fnode( '', $f, $c );
517 23         78 return $f;
518             }
519             elsif ( ref($h) eq 'ARRAY' ) {
520 0         0 my $fa = [];
521 0         0 foreach my $hx ( @{$h} ) {
  0         0  
522 0         0 my $f = {};
523 0         0 my $c = scpi_canon( $hx, $ov );
524 0         0 _scpi_fnode( '', $f, $c );
525 0         0 push( @{$fa}, $f );
  0         0  
526             }
527 0         0 return $fa;
528             }
529             else {
530 0         0 croak( "wrong type passed to scpi_flat:" . ref($h) );
531             }
532              
533             }
534              
535             sub _scpi_fnode {
536 142     142   194 my $fk = shift;
537 142         173 my $f = shift;
538 142         186 my $h = shift;
539              
540 142         178 my (@keys);
541 142 100       238 if ( ref($h) eq '' ) {
542 44         142 $fk =~ s/\:_VALUE$//;
543 44         123 $f->{$fk} = $h;
544 44         128 return;
545             }
546             else {
547 98         110 @keys = keys( %{$h} );
  98         216  
548 98 50       193 if (@keys) {
549 98 100       180 $fk .= ':' if $fk ne '';
550 98         133 foreach my $k (@keys) {
551 119         265 _scpi_fnode( "$fk$k", $f, $h->{$k} );
552             }
553             }
554             else {
555 0           $f->{$fk} = undef;
556             }
557             }
558             }
559              
560             1;
561              
562             __END__