diff --git a/NEWS b/NEWS index d08b2578..2aeeed12 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,14 @@ +20230322 + +New in this release: + +* Better support for wide characters in --latest-line. + +* Support for rsync 3.2.7. + +* Bug fixes and man page updates. + + 20230222 New in this release: diff --git a/doc/haikus b/doc/haikus index 0b081af2..c275e13d 100644 --- a/doc/haikus +++ b/doc/haikus @@ -10,9 +10,6 @@ Quote of the month: gnu parallel is actually like. really easy -- tom (era) @slimefiend@twitter - GNU parallel is magic, half of my work uses it, to the point where they're referenced and thanked in my thesis - -- Best Catboy Key Grip @alamogordoglass@twitter - Love to make a dual processor workstation absolutely whir running dozens of analysis scripts at once -- Best Catboy Key Grip @alamogordoglass@twitter @@ -233,6 +230,9 @@ https://negfeedback.blogspot.com/2020/05/indispensable-command-line-tools.html === Used === + GNU parallel is magic, half of my work uses it, to the point where they're referenced and thanked in my thesis + -- Best Catboy Key Grip @alamogordoglass@twitter + Praise GNU parallel, though. That gets me pretty far. -- Your Obed. Servant, J. B. @Jeffinatorator diff --git a/doc/release_new_version b/doc/release_new_version index f7572292..09af71e7 100644 --- a/doc/release_new_version +++ b/doc/release_new_version @@ -262,25 +262,27 @@ from:tange@gnu.org to:parallel@gnu.org, bug-parallel@gnu.org stable-bcc: Jesse Alama -Subject: GNU Parallel 20230322 ('Grækenland Larissa tog/Tiktok?') released +Subject: GNU Parallel 20230322 ('ICC Putin/Arrest Warrant/Fosbury/Grækenland Larissa tog/Tiktok?') released GNU Parallel 20230322 ('<<>>') has been released. It is available for download at: lbry://@GnuParallel:4 Quote of the month: - Praise GNU parallel, though. That gets me pretty far. - -- Your Obed. Servant, J. B. @Jeffinatorator + GNU parallel is magic, half of my work uses it, to the point where they're referenced and thanked in my thesis + -- Best Catboy Key Grip @alamogordoglass@twitter New in this release: -* parsort: --parallel now does closer to what you expect. +* Better support for wide characters in --latest-line. -* parallel: --files0 is --files but \0 separated. +* Support for rsync 3.2.7. * Bug fixes and man page updates. News about GNU Parallel: +* Analyzing multi-gigabyte JSON files locally https://thenybble.de/posts/json-analysis/ + * 5 great Perl scripts to keep in your sysadmin toolbox https://www.redhat.com/sysadmin/perl-scripts diff --git a/src/parallel b/src/parallel index 152c70fb..0e20692c 100755 --- a/src/parallel +++ b/src/parallel @@ -2503,6 +2503,7 @@ sub parse_options(@) { if(defined $opt::bar) { $opt::progress = $opt::bar; } if(defined $opt::bar or defined $opt::latestline) { my $fh = $Global::status_fd || *STDERR; + # Activate decode_utf8 eval q{ # Enable utf8 if possible use utf8; @@ -2515,6 +2516,20 @@ sub parse_options(@) { # UTF8-decode not supported: Dummy decode eval q{sub decode_utf8($;$) { $_[0]; }}; } + # Activate decode_utf8 + eval q{ + # Enable utf8 if possible + use utf8; + use Encode qw( encode_utf8 ); + use Text::CharWidth qw( mbswidth ); + use Unicode::Normalize qw( NFC NFD ); + }; + if(eval { mbswidth("ヌー平行") }) { + # Great: mbswidth works + } else { + # mbswidth not supported: Dummy mbswidth + eval q{ sub mbswidth { return length @_; } }; + } } # If you want GNU Parallel to be maintained in the future you @@ -2589,7 +2604,6 @@ sub parse_options(@) { # If you want GNU Parallel to be maintained in the future you # should keep this line. citation_notice(); - # This is because _YOU_ actively make it harder to justify # spending time developing GNU Parallel by removing it. @@ -4558,6 +4572,17 @@ sub progress() { } } +sub untabify($) { + # Convert \t into spaces + my @out; + my ($src); + # Deal with multi-byte characters + for my $src (split("\t",$_[0])) { + push @out, $src. " "x(8-mbswidth($src)%8); + } + return join "",@out; +} + # Prototype forwarding sub get_job_with_sshlogin($); sub get_job_with_sshlogin($) { @@ -11421,6 +11446,22 @@ sub print_files($) { } } + sub truncate_mbs($$) { + my $str = shift; + my $len = shift; + if(::mbswidth($str) == length($str)) { + $str = substr($str,0,$len); + } else { + # Highly inefficient truncator + while(::mbswidth($str) > $len) { + do { + chop $str; + } while(::mbswidth($str) < 0); + } + } + return $str; + } + sub print_latest_line($) { my $self = shift; my $out_fh = shift; @@ -11437,37 +11478,35 @@ sub print_files($) { eval q{ binmode $out_fh, "encoding(utf8)"; }; } my ($color,$reset_color) = $self->color(); - # Strings with TABs give the wrong length. Untabify strings my $termcol = ::terminal_columns(); - my $untabify_tag = ::decode_utf8($self->untabtag()); - my $taglen = length $untabify_tag; + my $untabify_tag = $self->untabtag(); + my $untabify_str = ::untabify($self->{$out_fh,'latestline'}); + # -1 to make space for $truncated_str + my $maxtaglen = $termcol - 1; + $untabify_tag = truncate_mbs($untabify_tag,$maxtaglen); + my $taglen = ::mbswidth($untabify_tag); + my $maxstrlen = $termcol - $taglen - 1; + $untabify_str = truncate_mbs($untabify_str,$maxstrlen); + my $strlen = ::mbswidth($untabify_str); my $truncated_tag = ""; - my $untabify_str = ::decode_utf8($self->{$out_fh,'latestline'}); - $untabify_str =~ s/\t/$tab{$-[0]%8}/g; - # -1 to make space for $truncated - my $strlen = $termcol - $taglen - 1; - my $strspc = $strlen - length $untabify_str; - if($strlen < 0) { $strlen = 0;} - # Line is shorter than terminal width: add " " - # Line is longer than terminal width: add ">" - my $truncated = ($strspc >= 0) ? " " : ">"; - if($taglen > $termcol) { - # Tag is longer than terminal width: add ">" to tag - # Remove $truncated (it will not be shown at all) - $taglen = $termcol - 1; + my $truncated_str = ""; + if($termcol - $taglen < 2) { $truncated_tag = ">"; - $truncated = ""; + } else { + if($termcol - $taglen - $strlen <= 2) { + $truncated_str = ">"; + } } $maxrow = ($row > $maxrow) ? $row : $maxrow; printf($out_fh ("%s%s%s%s". # up down \r eol - "%.${taglen}s%s". # tag trunc_tag - "%s%.${strlen}s%s%s". # color line trunc reset_color + "%s%s". # tag trunc_tag + "%s%s%s%s". # color line trunc reset_color "%s" # down ), "$up"x($currow - $row), "\n"x($row - $currow), "\r", $eol, - $untabify_tag,$truncated_tag, - $color, $untabify_str, $truncated, $reset_color, + ::decode_utf8($untabify_tag),$truncated_tag, + $color, ::decode_utf8($untabify_str), $truncated_str, $reset_color, "\n"x($maxrow - $row + 1)); $currow = $maxrow + 1; } @@ -11898,9 +11937,7 @@ sub untabtag($) { my $self = shift; my $tag = $self->tag(); if(not defined $self->{'untab'}{$tag}) { - my $t = $tag; - $t =~ s/\t/" "x(8-($-[0]%8))/eg; - $self->{'untab'}{$tag} = $t; + $self->{'untab'}{$tag} = ::untabify($tag); } return $self->{'untab'}{$tag}; } diff --git a/src/parallel.pod b/src/parallel.pod index 0d58522a..da245b6f 100644 --- a/src/parallel.pod +++ b/src/parallel.pod @@ -1586,9 +1586,9 @@ Similar to B<--memfree>. See also: B<--memfree> B<--load> -=item B<--latest-line> (beta testing) +=item B<--latest-line> (alpha testing) -=item B<--ll> (beta testing) +=item B<--ll> (alpha testing) Print the lastest line. Each job gets a single line that is updated with the lastest output from the job. @@ -1858,13 +1858,13 @@ Similar to B<--tty> but does not set B<--jobs> or B<--ungroup>. See also: B<--tty> -=item B<--output-as-files> (alpha testing) +=item B<--output-as-files> (beta testing) -=item B<--outputasfiles> (alpha testing) +=item B<--outputasfiles> (beta testing) -=item B<--files> (alpha testing) +=item B<--files> (beta testing) -=item B<--files0> (alpha testing) +=item B<--files0> (beta testing) Save output to files. diff --git a/src/parsort b/src/parsort index 43190f51..35dcb8d8 100755 --- a/src/parsort +++ b/src/parsort @@ -30,7 +30,7 @@ Same as B. Except: =over 4 -=item B<--parallel=>I (alpha testing) +=item B<--parallel=>I (beta testing) Change the number of sorts run concurrently to I. I will be increased to number of files if B is given more than I diff --git a/testsuite/input-files/perllib/Text/Abbrev.pm b/testsuite/input-files/perllib/Text/Abbrev.pm new file mode 100644 index 00000000..08143fea --- /dev/null +++ b/testsuite/input-files/perllib/Text/Abbrev.pm @@ -0,0 +1,83 @@ +package Text::Abbrev; +require 5.005; # Probably works on earlier versions too. +require Exporter; + +our $VERSION = '1.00'; + +=head1 NAME + +abbrev - create an abbreviation table from a list + +=head1 SYNOPSIS + + use Text::Abbrev; + abbrev $hashref, LIST + + +=head1 DESCRIPTION + +Stores all unambiguous truncations of each element of LIST +as keys in the associative array referenced by C<$hashref>. +The values are the original list elements. + +=head1 EXAMPLE + + $hashref = abbrev qw(list edit send abort gripe); + + %hash = abbrev qw(list edit send abort gripe); + + abbrev $hashref, qw(list edit send abort gripe); + + abbrev(*hash, qw(list edit send abort gripe)); + +=cut + +@ISA = qw(Exporter); +@EXPORT = qw(abbrev); + +# Usage: +# abbrev \%foo, LIST; +# ... +# $long = $foo{$short}; + +sub abbrev { + my ($word, $hashref, $glob, %table, $returnvoid); + + if (ref($_[0])) { # hash reference preferably + $hashref = shift; + $returnvoid = 1; + } elsif (ref \$_[0] eq 'GLOB') { # is actually a glob (deprecated) + $hashref = \%{shift()}; + $returnvoid = 1; + } + %{$hashref} = (); + + WORD: foreach $word (@_) { + for (my $len = (length $word) - 1; $len > 0; --$len) { + my $abbrev = substr($word,0,$len); + my $seen = ++$table{$abbrev}; + if ($seen == 1) { # We're the first word so far to have + # this abbreviation. + $hashref->{$abbrev} = $word; + } elsif ($seen == 2) { # We're the second word to have this + # abbreviation, so we can't use it. + delete $hashref->{$abbrev}; + } else { # We're the third word to have this + # abbreviation, so skip to the next word. + next WORD; + } + } + } + # Non-abbreviations always get entered, even if they aren't unique + foreach $word (@_) { + $hashref->{$word} = $word; + } + return if $returnvoid; + if (wantarray) { + %{$hashref}; + } else { + $hashref; + } +} + +1; diff --git a/testsuite/input-files/perllib/Text/Balanced.pm b/testsuite/input-files/perllib/Text/Balanced.pm new file mode 100644 index 00000000..ee83e545 --- /dev/null +++ b/testsuite/input-files/perllib/Text/Balanced.pm @@ -0,0 +1,2235 @@ +# EXTRACT VARIOUSLY DELIMITED TEXT SEQUENCES FROM STRINGS. +# FOR FULL DOCUMENTATION SEE Balanced.pod + +use 5.005; +use strict; + +package Text::Balanced; + +use Exporter; +use SelfLoader; +use vars qw { $VERSION @ISA %EXPORT_TAGS }; + +$VERSION = '1.89'; +@ISA = qw ( Exporter ); + +%EXPORT_TAGS = ( ALL => [ qw( + &extract_delimited + &extract_bracketed + &extract_quotelike + &extract_codeblock + &extract_variable + &extract_tagged + &extract_multiple + + &gen_delimited_pat + &gen_extract_tagged + + &delimited_pat + ) ] ); + +Exporter::export_ok_tags('ALL'); + +## +## These shenanagins are to avoid using $& in perl5.6+ +## +my $GetMatchedText = ($] < 5.006) ? eval 'sub { $& } ' + : eval 'sub { + substr($_[0], $-[0], $+[0] - $-[0]) + }'; + + +# PROTOTYPES + +sub _match_bracketed($$$$$$); +sub _match_variable($$); +sub _match_codeblock($$$$$$$); +sub _match_quotelike($$$$); + +# HANDLE RETURN VALUES IN VARIOUS CONTEXTS + +sub _failmsg { + my ($message, $pos) = @_; + $@ = bless { error=>$message, pos=>$pos }, "Text::Balanced::ErrorMsg"; +} + +sub _fail +{ + my ($wantarray, $textref, $message, $pos) = @_; + _failmsg $message, $pos if $message; + return ("",$$textref,"") if $wantarray; + return undef; +} + +sub _succeed +{ + $@ = undef; + my ($wantarray,$textref) = splice @_, 0, 2; + my ($extrapos, $extralen) = @_>18 ? splice(@_, -2, 2) : (0,0); + my ($startlen) = $_[5]; + my $remainderpos = $_[2]; + if ($wantarray) + { + my @res; + while (my ($from, $len) = splice @_, 0, 2) + { + push @res, substr($$textref,$from,$len); + } + if ($extralen) { # CORRECT FILLET + my $extra = substr($res[0], $extrapos-$startlen, $extralen, "\n"); + $res[1] = "$extra$res[1]"; + eval { substr($$textref,$remainderpos,0) = $extra; + substr($$textref,$extrapos,$extralen,"\n")} ; + #REARRANGE HERE DOC AND FILLET IF POSSIBLE + pos($$textref) = $remainderpos-$extralen+1; # RESET \G + } + else { + pos($$textref) = $remainderpos; # RESET \G + } + return @res; + } + else + { + my $match = substr($$textref,$_[0],$_[1]); + substr($match,$extrapos-$_[0]-$startlen,$extralen,"") if $extralen; + my $extra = $extralen + ? substr($$textref, $extrapos, $extralen)."\n" : ""; + eval {substr($$textref,$_[4],$_[1]+$_[5])=$extra} ; #CHOP OUT PREFIX & MATCH, IF POSSIBLE + pos($$textref) = $_[4]; # RESET \G + return $match; + } +} + +# BUILD A PATTERN MATCHING A SIMPLE DELIMITED STRING + +sub gen_delimited_pat($;$) # ($delimiters;$escapes) +{ + my ($dels, $escs) = @_; + return "" unless $dels =~ /\S/; + $escs = '\\' unless $escs; + $escs .= substr($escs,-1) x (length($dels)-length($escs)); + my @pat = (); + my $i; + for ($i=0; $i\0-\377/[[(({{</) + { + return _fail $wantarray, $textref, + "Did not find a suitable bracket in delimiter: \"$_[1]\"", + 0; + } + my $posbug = pos; + $ldel = join('|', map { quotemeta $_ } split('', $ldel)); + $rdel = join('|', map { quotemeta $_ } split('', $rdel)); + pos = $posbug; + + my $startpos = pos $$textref || 0; + my @match = _match_bracketed($textref,$pre, $ldel, $qdel, $quotelike, $rdel); + + return _fail ($wantarray, $textref) unless @match; + + return _succeed ( $wantarray, $textref, + $match[2], $match[5]+2, # MATCH + @match[8,9], # REMAINDER + @match[0,1], # PREFIX + ); +} + +sub _match_bracketed($$$$$$) # $textref, $pre, $ldel, $qdel, $quotelike, $rdel +{ + my ($textref, $pre, $ldel, $qdel, $quotelike, $rdel) = @_; + my ($startpos, $ldelpos, $endpos) = (pos $$textref = pos $$textref||0); + unless ($$textref =~ m/\G$pre/gc) + { + _failmsg "Did not find prefix: /$pre/", $startpos; + return; + } + + $ldelpos = pos $$textref; + + unless ($$textref =~ m/\G($ldel)/gc) + { + _failmsg "Did not find opening bracket after prefix: \"$pre\"", + pos $$textref; + pos $$textref = $startpos; + return; + } + + my @nesting = ( $1 ); + my $textlen = length $$textref; + while (pos $$textref < $textlen) + { + next if $$textref =~ m/\G\\./gcs; + + if ($$textref =~ m/\G($ldel)/gc) + { + push @nesting, $1; + } + elsif ($$textref =~ m/\G($rdel)/gc) + { + my ($found, $brackettype) = ($1, $1); + if ($#nesting < 0) + { + _failmsg "Unmatched closing bracket: \"$found\"", + pos $$textref; + pos $$textref = $startpos; + return; + } + my $expected = pop(@nesting); + $expected =~ tr/({[/; + if ($expected ne $brackettype) + { + _failmsg qq{Mismatched closing bracket: expected "$expected" but found "$found"}, + pos $$textref; + pos $$textref = $startpos; + return; + } + last if $#nesting < 0; + } + elsif ($qdel && $$textref =~ m/\G([$qdel])/gc) + { + $$textref =~ m/\G[^\\$1]*(?:\\.[^\\$1]*)*(\Q$1\E)/gsc and next; + _failmsg "Unmatched embedded quote ($1)", + pos $$textref; + pos $$textref = $startpos; + return; + } + elsif ($quotelike && _match_quotelike($textref,"",1,0)) + { + next; + } + + else { $$textref =~ m/\G(?:[a-zA-Z0-9]+|.)/gcs } + } + if ($#nesting>=0) + { + _failmsg "Unmatched opening bracket(s): " + . join("..",@nesting)."..", + pos $$textref; + pos $$textref = $startpos; + return; + } + + $endpos = pos $$textref; + + return ( + $startpos, $ldelpos-$startpos, # PREFIX + $ldelpos, 1, # OPENING BRACKET + $ldelpos+1, $endpos-$ldelpos-2, # CONTENTS + $endpos-1, 1, # CLOSING BRACKET + $endpos, length($$textref)-$endpos, # REMAINDER + ); +} + +sub revbracket($) +{ + my $brack = reverse $_[0]; + $brack =~ tr/[({/; + return $brack; +} + +my $XMLNAME = q{[a-zA-Z_:][a-zA-Z0-9_:.-]*}; + +sub extract_tagged (;$$$$$) # ($text, $opentag, $closetag, $pre, \%options) +{ + my $textref = defined $_[0] ? \$_[0] : \$_; + my $ldel = $_[1]; + my $rdel = $_[2]; + my $pre = defined $_[3] ? $_[3] : '\s*'; + my %options = defined $_[4] ? %{$_[4]} : (); + my $omode = defined $options{fail} ? $options{fail} : ''; + my $bad = ref($options{reject}) eq 'ARRAY' ? join('|', @{$options{reject}}) + : defined($options{reject}) ? $options{reject} + : '' + ; + my $ignore = ref($options{ignore}) eq 'ARRAY' ? join('|', @{$options{ignore}}) + : defined($options{ignore}) ? $options{ignore} + : '' + ; + + if (!defined $ldel) { $ldel = '<\w+(?:' . gen_delimited_pat(q{'"}) . '|[^>])*>'; } + $@ = undef; + + my @match = _match_tagged($textref, $pre, $ldel, $rdel, $omode, $bad, $ignore); + + return _fail(wantarray, $textref) unless @match; + return _succeed wantarray, $textref, + $match[2], $match[3]+$match[5]+$match[7], # MATCH + @match[8..9,0..1,2..7]; # REM, PRE, BITS +} + +sub _match_tagged # ($$$$$$$) +{ + my ($textref, $pre, $ldel, $rdel, $omode, $bad, $ignore) = @_; + my $rdelspec; + + my ($startpos, $opentagpos, $textpos, $parapos, $closetagpos, $endpos) = ( pos($$textref) = pos($$textref)||0 ); + + unless ($$textref =~ m/\G($pre)/gc) + { + _failmsg "Did not find prefix: /$pre/", pos $$textref; + goto failed; + } + + $opentagpos = pos($$textref); + + unless ($$textref =~ m/\G$ldel/gc) + { + _failmsg "Did not find opening tag: /$ldel/", pos $$textref; + goto failed; + } + + $textpos = pos($$textref); + + if (!defined $rdel) + { + $rdelspec = &$GetMatchedText($$textref); + + unless ($rdelspec =~ s/\A([[(<{]+)($XMLNAME).*/ quotemeta "$1\/$2". revbracket($1) /oes) + { + _failmsg "Unable to construct closing tag to match: $rdel", + pos $$textref; + goto failed; + } + } + else + { + $rdelspec = eval "qq{$rdel}"; + } + + while (pos($$textref) < length($$textref)) + { + next if $$textref =~ m/\G\\./gc; + + if ($$textref =~ m/\G(\n[ \t]*\n)/gc ) + { + $parapos = pos($$textref) - length($1) + unless defined $parapos; + } + elsif ($$textref =~ m/\G($rdelspec)/gc ) + { + $closetagpos = pos($$textref)-length($1); + goto matched; + } + elsif ($ignore && $$textref =~ m/\G(?:$ignore)/gc) + { + next; + } + elsif ($bad && $$textref =~ m/\G($bad)/gcs) + { + pos($$textref) -= length($1); # CUT OFF WHATEVER CAUSED THE SHORTNESS + goto short if ($omode eq 'PARA' || $omode eq 'MAX'); + _failmsg "Found invalid nested tag: $1", pos $$textref; + goto failed; + } + elsif ($$textref =~ m/\G($ldel)/gc) + { + my $tag = $1; + pos($$textref) -= length($tag); # REWIND TO NESTED TAG + unless (_match_tagged(@_)) # MATCH NESTED TAG + { + goto short if $omode eq 'PARA' || $omode eq 'MAX'; + _failmsg "Found unbalanced nested tag: $tag", + pos $$textref; + goto failed; + } + } + else { $$textref =~ m/./gcs } + } + +short: + $closetagpos = pos($$textref); + goto matched if $omode eq 'MAX'; + goto failed unless $omode eq 'PARA'; + + if (defined $parapos) { pos($$textref) = $parapos } + else { $parapos = pos($$textref) } + + return ( + $startpos, $opentagpos-$startpos, # PREFIX + $opentagpos, $textpos-$opentagpos, # OPENING TAG + $textpos, $parapos-$textpos, # TEXT + $parapos, 0, # NO CLOSING TAG + $parapos, length($$textref)-$parapos, # REMAINDER + ); + +matched: + $endpos = pos($$textref); + return ( + $startpos, $opentagpos-$startpos, # PREFIX + $opentagpos, $textpos-$opentagpos, # OPENING TAG + $textpos, $closetagpos-$textpos, # TEXT + $closetagpos, $endpos-$closetagpos, # CLOSING TAG + $endpos, length($$textref)-$endpos, # REMAINDER + ); + +failed: + _failmsg "Did not find closing tag", pos $$textref unless $@; + pos($$textref) = $startpos; + return; +} + +sub extract_variable (;$$) +{ + my $textref = defined $_[0] ? \$_[0] : \$_; + return ("","","") unless defined $$textref; + my $pre = defined $_[1] ? $_[1] : '\s*'; + + my @match = _match_variable($textref,$pre); + + return _fail wantarray, $textref unless @match; + + return _succeed wantarray, $textref, + @match[2..3,4..5,0..1]; # MATCH, REMAINDER, PREFIX +} + +sub _match_variable($$) +{ +# $# +# $^ +# $$ + my ($textref, $pre) = @_; + my $startpos = pos($$textref) = pos($$textref)||0; + unless ($$textref =~ m/\G($pre)/gc) + { + _failmsg "Did not find prefix: /$pre/", pos $$textref; + return; + } + my $varpos = pos($$textref); + unless ($$textref =~ m{\G\$\s*(\d+|[][&`'+*./|,";%=~:?!\@<>()-]|\^[a-z]?)}gci) + { + unless ($$textref =~ m/\G((\$#?|[*\@\%]|\\&)+)/gc) + { + _failmsg "Did not find leading dereferencer", pos $$textref; + pos $$textref = $startpos; + return; + } + my $deref = $1; + + unless ($$textref =~ m/\G\s*(?:::|')?(?:[_a-z]\w*(?:::|'))*[_a-z]\w*/gci + or _match_codeblock($textref, "", '\{', '\}', '\{', '\}', 0) + or $deref eq '$#' or $deref eq '$$' ) + { + _failmsg "Bad identifier after dereferencer", pos $$textref; + pos $$textref = $startpos; + return; + } + } + + while (1) + { + next if _match_codeblock($textref, + qr/\s*->\s*(?:[_a-zA-Z]\w+\s*)?/, + qr/[({[]/, qr/[)}\]]/, + qr/[({[]/, qr/[)}\]]/, 0); + next if _match_codeblock($textref, + qr/\s*/, qr/[{[]/, qr/[}\]]/, + qr/[{[]/, qr/[}\]]/, 0); + next if _match_variable($textref,'\s*->\s*'); + next if $$textref =~ m/\G\s*->\s*\w+(?![{([])/gc; + last; + } + + my $endpos = pos($$textref); + return ($startpos, $varpos-$startpos, + $varpos, $endpos-$varpos, + $endpos, length($$textref)-$endpos + ); +} + +sub extract_codeblock (;$$$$$) +{ + my $textref = defined $_[0] ? \$_[0] : \$_; + my $wantarray = wantarray; + my $ldel_inner = defined $_[1] ? $_[1] : '{'; + my $pre = defined $_[2] ? $_[2] : '\s*'; + my $ldel_outer = defined $_[3] ? $_[3] : $ldel_inner; + my $rd = $_[4]; + my $rdel_inner = $ldel_inner; + my $rdel_outer = $ldel_outer; + my $posbug = pos; + for ($ldel_inner, $ldel_outer) { tr/[]()<>{}\0-\377/[[((<<{{/ds } + for ($rdel_inner, $rdel_outer) { tr/[]()<>{}\0-\377/]]))>>}}/ds } + for ($ldel_inner, $ldel_outer, $rdel_inner, $rdel_outer) + { + $_ = '('.join('|',map { quotemeta $_ } split('',$_)).')' + } + pos = $posbug; + + my @match = _match_codeblock($textref, $pre, + $ldel_outer, $rdel_outer, + $ldel_inner, $rdel_inner, + $rd); + return _fail($wantarray, $textref) unless @match; + return _succeed($wantarray, $textref, + @match[2..3,4..5,0..1] # MATCH, REMAINDER, PREFIX + ); + +} + +sub _match_codeblock($$$$$$$) +{ + my ($textref, $pre, $ldel_outer, $rdel_outer, $ldel_inner, $rdel_inner, $rd) = @_; + my $startpos = pos($$textref) = pos($$textref) || 0; + unless ($$textref =~ m/\G($pre)/gc) + { + _failmsg qq{Did not match prefix /$pre/ at"} . + substr($$textref,pos($$textref),20) . + q{..."}, + pos $$textref; + return; + } + my $codepos = pos($$textref); + unless ($$textref =~ m/\G($ldel_outer)/gc) # OUTERMOST DELIMITER + { + _failmsg qq{Did not find expected opening bracket at "} . + substr($$textref,pos($$textref),20) . + q{..."}, + pos $$textref; + pos $$textref = $startpos; + return; + } + my $closing = $1; + $closing =~ tr/([<{/)]>}/; + my $matched; + my $patvalid = 1; + while (pos($$textref) < length($$textref)) + { + $matched = ''; + if ($rd && $$textref =~ m#\G(\Q(?)\E|\Q(s?)\E|\Q(s)\E)#gc) + { + $patvalid = 0; + next; + } + + if ($$textref =~ m/\G\s*#.*/gc) + { + next; + } + + if ($$textref =~ m/\G\s*($rdel_outer)/gc) + { + unless ($matched = ($closing && $1 eq $closing) ) + { + next if $1 eq '>'; # MIGHT BE A "LESS THAN" + _failmsg q{Mismatched closing bracket at "} . + substr($$textref,pos($$textref),20) . + qq{...". Expected '$closing'}, + pos $$textref; + } + last; + } + + if (_match_variable($textref,'\s*') || + _match_quotelike($textref,'\s*',$patvalid,$patvalid) ) + { + $patvalid = 0; + next; + } + + + # NEED TO COVER MANY MORE CASES HERE!!! + if ($$textref =~ m#\G\s*( [-+*x/%^&|.]=? + | [!=]~ + | =(?!>) + | (\*\*|&&|\|\||<<|>>)=? + | split|grep|map|return + )#gcx) + { + $patvalid = 1; + next; + } + + if ( _match_codeblock($textref, '\s*', $ldel_inner, $rdel_inner, $ldel_inner, $rdel_inner, $rd) ) + { + $patvalid = 1; + next; + } + + if ($$textref =~ m/\G\s*$ldel_outer/gc) + { + _failmsg q{Improperly nested codeblock at "} . + substr($$textref,pos($$textref),20) . + q{..."}, + pos $$textref; + last; + } + + $patvalid = 0; + $$textref =~ m/\G\s*(\w+|[-=>]>|.|\Z)/gc; + } + continue { $@ = undef } + + unless ($matched) + { + _failmsg 'No match found for opening bracket', pos $$textref + unless $@; + return; + } + + my $endpos = pos($$textref); + return ( $startpos, $codepos-$startpos, + $codepos, $endpos-$codepos, + $endpos, length($$textref)-$endpos, + ); +} + + +my %mods = ( + 'none' => '[cgimsox]*', + 'm' => '[cgimsox]*', + 's' => '[cegimsox]*', + 'tr' => '[cds]*', + 'y' => '[cds]*', + 'qq' => '', + 'qx' => '', + 'qw' => '', + 'qr' => '[imsx]*', + 'q' => '', + ); + +sub extract_quotelike (;$$) +{ + my $textref = $_[0] ? \$_[0] : \$_; + my $wantarray = wantarray; + my $pre = defined $_[1] ? $_[1] : '\s*'; + + my @match = _match_quotelike($textref,$pre,1,0); + return _fail($wantarray, $textref) unless @match; + return _succeed($wantarray, $textref, + $match[2], $match[18]-$match[2], # MATCH + @match[18,19], # REMAINDER + @match[0,1], # PREFIX + @match[2..17], # THE BITS + @match[20,21], # ANY FILLET? + ); +}; + +sub _match_quotelike($$$$) # ($textref, $prepat, $allow_raw_match) +{ + my ($textref, $pre, $rawmatch, $qmark) = @_; + + my ($textlen,$startpos, + $oppos, + $preld1pos,$ld1pos,$str1pos,$rd1pos, + $preld2pos,$ld2pos,$str2pos,$rd2pos, + $modpos) = ( length($$textref), pos($$textref) = pos($$textref) || 0 ); + + unless ($$textref =~ m/\G($pre)/gc) + { + _failmsg qq{Did not find prefix /$pre/ at "} . + substr($$textref, pos($$textref), 20) . + q{..."}, + pos $$textref; + return; + } + $oppos = pos($$textref); + + my $initial = substr($$textref,$oppos,1); + + if ($initial && $initial =~ m|^[\"\'\`]| + || $rawmatch && $initial =~ m|^/| + || $qmark && $initial =~ m|^\?|) + { + unless ($$textref =~ m/ \Q$initial\E [^\\$initial]* (\\.[^\\$initial]*)* \Q$initial\E /gcsx) + { + _failmsg qq{Did not find closing delimiter to match '$initial' at "} . + substr($$textref, $oppos, 20) . + q{..."}, + pos $$textref; + pos $$textref = $startpos; + return; + } + $modpos= pos($$textref); + $rd1pos = $modpos-1; + + if ($initial eq '/' || $initial eq '?') + { + $$textref =~ m/\G$mods{none}/gc + } + + my $endpos = pos($$textref); + return ( + $startpos, $oppos-$startpos, # PREFIX + $oppos, 0, # NO OPERATOR + $oppos, 1, # LEFT DEL + $oppos+1, $rd1pos-$oppos-1, # STR/PAT + $rd1pos, 1, # RIGHT DEL + $modpos, 0, # NO 2ND LDEL + $modpos, 0, # NO 2ND STR + $modpos, 0, # NO 2ND RDEL + $modpos, $endpos-$modpos, # MODIFIERS + $endpos, $textlen-$endpos, # REMAINDER + ); + } + + unless ($$textref =~ m{\G((?:m|s|qq|qx|qw|q|qr|tr|y)\b(?=\s*\S)|<<)}gc) + { + _failmsg q{No quotelike operator found after prefix at "} . + substr($$textref, pos($$textref), 20) . + q{..."}, + pos $$textref; + pos $$textref = $startpos; + return; + } + + my $op = $1; + $preld1pos = pos($$textref); + if ($op eq '<<') { + $ld1pos = pos($$textref); + my $label; + if ($$textref =~ m{\G([A-Za-z_]\w*)}gc) { + $label = $1; + } + elsif ($$textref =~ m{ \G ' ([^'\\]* (?:\\.[^'\\]*)*) ' + | \G " ([^"\\]* (?:\\.[^"\\]*)*) " + | \G ` ([^`\\]* (?:\\.[^`\\]*)*) ` + }gcsx) { + $label = $+; + } + else { + $label = ""; + } + my $extrapos = pos($$textref); + $$textref =~ m{.*\n}gc; + $str1pos = pos($$textref); + unless ($$textref =~ m{.*?\n(?=$label\n)}gc) { + _failmsg qq{Missing here doc terminator ('$label') after "} . + substr($$textref, $startpos, 20) . + q{..."}, + pos $$textref; + pos $$textref = $startpos; + return; + } + $rd1pos = pos($$textref); + $$textref =~ m{$label\n}gc; + $ld2pos = pos($$textref); + return ( + $startpos, $oppos-$startpos, # PREFIX + $oppos, length($op), # OPERATOR + $ld1pos, $extrapos-$ld1pos, # LEFT DEL + $str1pos, $rd1pos-$str1pos, # STR/PAT + $rd1pos, $ld2pos-$rd1pos, # RIGHT DEL + $ld2pos, 0, # NO 2ND LDEL + $ld2pos, 0, # NO 2ND STR + $ld2pos, 0, # NO 2ND RDEL + $ld2pos, 0, # NO MODIFIERS + $ld2pos, $textlen-$ld2pos, # REMAINDER + $extrapos, $str1pos-$extrapos, # FILLETED BIT + ); + } + + $$textref =~ m/\G\s*/gc; + $ld1pos = pos($$textref); + $str1pos = $ld1pos+1; + + unless ($$textref =~ m/\G(\S)/gc) # SHOULD USE LOOKAHEAD + { + _failmsg "No block delimiter found after quotelike $op", + pos $$textref; + pos $$textref = $startpos; + return; + } + pos($$textref) = $ld1pos; # HAVE TO DO THIS BECAUSE LOOKAHEAD BROKEN + my ($ldel1, $rdel1) = ("\Q$1","\Q$1"); + if ($ldel1 =~ /[[(<{]/) + { + $rdel1 =~ tr/[({/; + _match_bracketed($textref,"",$ldel1,"","",$rdel1) + || do { pos $$textref = $startpos; return }; + } + else + { + $$textref =~ /$ldel1[^\\$ldel1]*(\\.[^\\$ldel1]*)*$ldel1/gcs + || do { pos $$textref = $startpos; return }; + } + $ld2pos = $rd1pos = pos($$textref)-1; + + my $second_arg = $op =~ /s|tr|y/ ? 1 : 0; + if ($second_arg) + { + my ($ldel2, $rdel2); + if ($ldel1 =~ /[[(<{]/) + { + unless ($$textref =~ /\G\s*(\S)/gc) # SHOULD USE LOOKAHEAD + { + _failmsg "Missing second block for quotelike $op", + pos $$textref; + pos $$textref = $startpos; + return; + } + $ldel2 = $rdel2 = "\Q$1"; + $rdel2 =~ tr/[({/; + } + else + { + $ldel2 = $rdel2 = $ldel1; + } + $str2pos = $ld2pos+1; + + if ($ldel2 =~ /[[(<{]/) + { + pos($$textref)--; # OVERCOME BROKEN LOOKAHEAD + _match_bracketed($textref,"",$ldel2,"","",$rdel2) + || do { pos $$textref = $startpos; return }; + } + else + { + $$textref =~ /[^\\$ldel2]*(\\.[^\\$ldel2]*)*$ldel2/gcs + || do { pos $$textref = $startpos; return }; + } + $rd2pos = pos($$textref)-1; + } + else + { + $ld2pos = $str2pos = $rd2pos = $rd1pos; + } + + $modpos = pos $$textref; + + $$textref =~ m/\G($mods{$op})/gc; + my $endpos = pos $$textref; + + return ( + $startpos, $oppos-$startpos, # PREFIX + $oppos, length($op), # OPERATOR + $ld1pos, 1, # LEFT DEL + $str1pos, $rd1pos-$str1pos, # STR/PAT + $rd1pos, 1, # RIGHT DEL + $ld2pos, $second_arg, # 2ND LDEL (MAYBE) + $str2pos, $rd2pos-$str2pos, # 2ND STR (MAYBE) + $rd2pos, $second_arg, # 2ND RDEL (MAYBE) + $modpos, $endpos-$modpos, # MODIFIERS + $endpos, $textlen-$endpos, # REMAINDER + ); +} + +my $def_func = +[ + sub { extract_variable($_[0], '') }, + sub { extract_quotelike($_[0],'') }, + sub { extract_codeblock($_[0],'{}','') }, +]; + +sub extract_multiple (;$$$$) # ($text, $functions_ref, $max_fields, $ignoreunknown) +{ + my $textref = defined($_[0]) ? \$_[0] : \$_; + my $posbug = pos; + my ($lastpos, $firstpos); + my @fields = (); + + #for ($$textref) + { + my @func = defined $_[1] ? @{$_[1]} : @{$def_func}; + my $max = defined $_[2] && $_[2]>0 ? $_[2] : 1_000_000_000; + my $igunk = $_[3]; + + pos $$textref ||= 0; + + unless (wantarray) + { + use Carp; + carp "extract_multiple reset maximal count to 1 in scalar context" + if $^W && defined($_[2]) && $max > 1; + $max = 1 + } + + my $unkpos; + my $func; + my $class; + + my @class; + foreach $func ( @func ) + { + if (ref($func) eq 'HASH') + { + push @class, (keys %$func)[0]; + $func = (values %$func)[0]; + } + else + { + push @class, undef; + } + } + + FIELD: while (pos($$textref) < length($$textref)) + { + my $field; + my @bits; + foreach my $i ( 0..$#func ) + { + my $pref; + $func = $func[$i]; + $class = $class[$i]; + $lastpos = pos $$textref; + if (ref($func) eq 'CODE') + { ($field,undef,$pref) = @bits = $func->($$textref) } + elsif (ref($func) eq 'Text::Balanced::Extractor') + { @bits = $field = $func->extract($$textref) } + elsif( $$textref =~ m/\G$func/gc ) + { @bits = $field = defined($1) ? $1 : &$GetMatchedText($$textref) } + # substr() on previous line is "$&", without the pain + $pref ||= ""; + if (defined($field) && length($field)) + { + if (!$igunk) { + $unkpos = pos $$textref + if length($pref) && !defined($unkpos); + if (defined $unkpos) + { + push @fields, substr($$textref, $unkpos, $lastpos-$unkpos).$pref; + $firstpos = $unkpos unless defined $firstpos; + undef $unkpos; + last FIELD if @fields == $max; + } + } + push @fields, $class + ? bless (\$field, $class) + : $field; + $firstpos = $lastpos unless defined $firstpos; + $lastpos = pos $$textref; + last FIELD if @fields == $max; + next FIELD; + } + } + if ($$textref =~ /\G(.)/gcs) + { + $unkpos = pos($$textref)-1 + unless $igunk || defined $unkpos; + } + } + + if (defined $unkpos) + { + push @fields, substr($$textref, $unkpos); + $firstpos = $unkpos unless defined $firstpos; + $lastpos = length $$textref; + } + last; + } + + pos $$textref = $lastpos; + return @fields if wantarray; + + $firstpos ||= 0; + eval { substr($$textref,$firstpos,$lastpos-$firstpos)=""; + pos $$textref = $firstpos }; + return $fields[0]; +} + + +sub gen_extract_tagged # ($opentag, $closetag, $pre, \%options) +{ + my $ldel = $_[0]; + my $rdel = $_[1]; + my $pre = defined $_[2] ? $_[2] : '\s*'; + my %options = defined $_[3] ? %{$_[3]} : (); + my $omode = defined $options{fail} ? $options{fail} : ''; + my $bad = ref($options{reject}) eq 'ARRAY' ? join('|', @{$options{reject}}) + : defined($options{reject}) ? $options{reject} + : '' + ; + my $ignore = ref($options{ignore}) eq 'ARRAY' ? join('|', @{$options{ignore}}) + : defined($options{ignore}) ? $options{ignore} + : '' + ; + + if (!defined $ldel) { $ldel = '<\w+(?:' . gen_delimited_pat(q{'"}) . '|[^>])*>'; } + + my $posbug = pos; + for ($ldel, $pre, $bad, $ignore) { $_ = qr/$_/ if $_ } + pos = $posbug; + + my $closure = sub + { + my $textref = defined $_[0] ? \$_[0] : \$_; + my @match = Text::Balanced::_match_tagged($textref, $pre, $ldel, $rdel, $omode, $bad, $ignore); + + return _fail(wantarray, $textref) unless @match; + return _succeed wantarray, $textref, + $match[2], $match[3]+$match[5]+$match[7], # MATCH + @match[8..9,0..1,2..7]; # REM, PRE, BITS + }; + + bless $closure, 'Text::Balanced::Extractor'; +} + +package Text::Balanced::Extractor; + +sub extract($$) # ($self, $text) +{ + &{$_[0]}($_[1]); +} + +package Text::Balanced::ErrorMsg; + +use overload '""' => sub { "$_[0]->{error}, detected at offset $_[0]->{pos}" }; + +1; + +__END__ + +=head1 NAME + +Text::Balanced - Extract delimited text sequences from strings. + + +=head1 SYNOPSIS + + use Text::Balanced qw ( + extract_delimited + extract_bracketed + extract_quotelike + extract_codeblock + extract_variable + extract_tagged + extract_multiple + + gen_delimited_pat + gen_extract_tagged + ); + + # Extract the initial substring of $text that is delimited by + # two (unescaped) instances of the first character in $delim. + + ($extracted, $remainder) = extract_delimited($text,$delim); + + + # Extract the initial substring of $text that is bracketed + # with a delimiter(s) specified by $delim (where the string + # in $delim contains one or more of '(){}[]<>'). + + ($extracted, $remainder) = extract_bracketed($text,$delim); + + + # Extract the initial substring of $text that is bounded by + # an HTML/XML tag. + + ($extracted, $remainder) = extract_tagged($text); + + + # Extract the initial substring of $text that is bounded by + # a C...C pair. Don't allow nested C tags + + ($extracted, $remainder) = + extract_tagged($text,"BEGIN","END",undef,{bad=>["BEGIN"]}); + + + # Extract the initial substring of $text that represents a + # Perl "quote or quote-like operation" + + ($extracted, $remainder) = extract_quotelike($text); + + + # Extract the initial substring of $text that represents a block + # of Perl code, bracketed by any of character(s) specified by $delim + # (where the string $delim contains one or more of '(){}[]<>'). + + ($extracted, $remainder) = extract_codeblock($text,$delim); + + + # Extract the initial substrings of $text that would be extracted by + # one or more sequential applications of the specified functions + # or regular expressions + + @extracted = extract_multiple($text, + [ \&extract_bracketed, + \&extract_quotelike, + \&some_other_extractor_sub, + qr/[xyz]*/, + 'literal', + ]); + +# Create a string representing an optimized pattern (a la Friedl) +# that matches a substring delimited by any of the specified characters +# (in this case: any type of quote or a slash) + + $patstring = gen_delimited_pat(q{'"`/}); + + +# Generate a reference to an anonymous sub that is just like extract_tagged +# but pre-compiled and optimized for a specific pair of tags, and consequently +# much faster (i.e. 3 times faster). It uses qr// for better performance on +# repeated calls, so it only works under Perl 5.005 or later. + + $extract_head = gen_extract_tagged('',''); + + ($extracted, $remainder) = $extract_head->($text); + + +=head1 DESCRIPTION + +The various C subroutines may be used to extract a +delimited string (possibly after skipping a specified prefix string). +The search for the string always begins at the current C +location of the string's variable (or at index zero, if no C +position is defined). + +=head2 General behaviour in list contexts + +In a list context, all the subroutines return a list, the first three +elements of which are always: + +=over 4 + +=item [0] + +The extracted string, including the specified delimiters. +If the extraction fails an empty string is returned. + +=item [1] + +The remainder of the input string (i.e. the characters after the +extracted string). On failure, the entire string is returned. + +=item [2] + +The skipped prefix (i.e. the characters before the extracted string). +On failure, the empty string is returned. + +=back + +Note that in a list context, the contents of the original input text (the first +argument) are not modified in any way. + +However, if the input text was passed in a variable, that variable's +C value is updated to point at the first character after the +extracted text. That means that in a list context the various +subroutines can be used much like regular expressions. For example: + + while ( $next = (extract_quotelike($text))[0] ) + { + # process next quote-like (in $next) + } + + +=head2 General behaviour in scalar and void contexts + +In a scalar context, the extracted string is returned, having first been +removed from the input text. Thus, the following code also processes +each quote-like operation, but actually removes them from $text: + + while ( $next = extract_quotelike($text) ) + { + # process next quote-like (in $next) + } + +Note that if the input text is a read-only string (i.e. a literal), +no attempt is made to remove the extracted text. + +In a void context the behaviour of the extraction subroutines is +exactly the same as in a scalar context, except (of course) that the +extracted substring is not returned. + +=head2 A note about prefixes + +Prefix patterns are matched without any trailing modifiers (C etc.) +This can bite you if you're expecting a prefix specification like +'.*?(?=

)' to skip everything up to the first

tag. Such a prefix +pattern will only succeed if the

tag is on the current line, since +. normally doesn't match newlines. + +To overcome this limitation, you need to turn on /s matching within +the prefix pattern, using the C<(?s)> directive: '(?s).*?(?=

)' + + +=head2 C + +The C function formalizes the common idiom +of extracting a single-character-delimited substring from the start of +a string. For example, to extract a single-quote delimited string, the +following code is typically used: + + ($remainder = $text) =~ s/\A('(\\.|[^'])*')//s; + $extracted = $1; + +but with C it can be simplified to: + + ($extracted,$remainder) = extract_delimited($text, "'"); + +C takes up to four scalars (the input text, the +delimiters, a prefix pattern to be skipped, and any escape characters) +and extracts the initial substring of the text that +is appropriately delimited. If the delimiter string has multiple +characters, the first one encountered in the text is taken to delimit +the substring. +The third argument specifies a prefix pattern that is to be skipped +(but must be present!) before the substring is extracted. +The final argument specifies the escape character to be used for each +delimiter. + +All arguments are optional. If the escape characters are not specified, +every delimiter is escaped with a backslash (C<\>). +If the prefix is not specified, the +pattern C<'\s*'> - optional whitespace - is used. If the delimiter set +is also not specified, the set C is used. If the text to be processed +is not specified either, C<$_> is used. + +In list context, C returns an array of three +elements, the extracted substring (I), the remainder of the text, and the skipped prefix (if +any). If a suitable delimited substring is not found, the first +element of the array is the empty string, the second is the complete +original text, and the prefix returned in the third element is an +empty string. + +In a scalar context, just the extracted substring is returned. In +a void context, the extracted substring (and any prefix) are simply +removed from the beginning of the first argument. + +Examples: + + # Remove a single-quoted substring from the very beginning of $text: + + $substring = extract_delimited($text, "'", ''); + + # Remove a single-quoted Pascalish substring (i.e. one in which + # doubling the quote character escapes it) from the very + # beginning of $text: + + $substring = extract_delimited($text, "'", '', "'"); + + # Extract a single- or double- quoted substring from the + # beginning of $text, optionally after some whitespace + # (note the list context to protect $text from modification): + + ($substring) = extract_delimited $text, q{"'}; + + + # Delete the substring delimited by the first '/' in $text: + + $text = join '', (extract_delimited($text,'/','[^/]*')[2,1]; + +Note that this last example is I the same as deleting the first +quote-like pattern. For instance, if C<$text> contained the string: + + "if ('./cmd' =~ m/$UNIXCMD/s) { $cmd = $1; }" + +then after the deletion it would contain: + + "if ('.$UNIXCMD/s) { $cmd = $1; }" + +not: + + "if ('./cmd' =~ ms) { $cmd = $1; }" + + +See L<"extract_quotelike"> for a (partial) solution to this problem. + + +=head2 C + +Like C<"extract_delimited">, the C function takes +up to three optional scalar arguments: a string to extract from, a delimiter +specifier, and a prefix pattern. As before, a missing prefix defaults to +optional whitespace and a missing text defaults to C<$_>. However, a missing +delimiter specifier defaults to C<'{}()[]EE'> (see below). + +C extracts a balanced-bracket-delimited +substring (using any one (or more) of the user-specified delimiter +brackets: '(..)', '{..}', '[..]', or '<..>'). Optionally it will also +respect quoted unbalanced brackets (see below). + +A "delimiter bracket" is a bracket in list of delimiters passed as +C's second argument. Delimiter brackets are +specified by giving either the left or right (or both!) versions +of the required bracket(s). Note that the order in which +two or more delimiter brackets are specified is not significant. + +A "balanced-bracket-delimited substring" is a substring bounded by +matched brackets, such that any other (left or right) delimiter +bracket I the substring is also matched by an opposite +(right or left) delimiter bracket I. Any +type of bracket not in the delimiter list is treated as an ordinary +character. + +In other words, each type of bracket specified as a delimiter must be +balanced and correctly nested within the substring, and any other kind of +("non-delimiter") bracket in the substring is ignored. + +For example, given the string: + + $text = "{ an '[irregularly :-(] {} parenthesized >:-)' string }"; + +then a call to C in a list context: + + @result = extract_bracketed( $text, '{}' ); + +would return: + + ( "{ an '[irregularly :-(] {} parenthesized >:-)' string }" , "" , "" ) + +since both sets of C<'{..}'> brackets are properly nested and evenly balanced. +(In a scalar context just the first element of the array would be returned. In +a void context, C<$text> would be replaced by an empty string.) + +Likewise the call in: + + @result = extract_bracketed( $text, '{[' ); + +would return the same result, since all sets of both types of specified +delimiter brackets are correctly nested and balanced. + +However, the call in: + + @result = extract_bracketed( $text, '{([<' ); + +would fail, returning: + + ( undef , "{ an '[irregularly :-(] {} parenthesized >:-)' string }" ); + +because the embedded pairs of C<'(..)'>s and C<'[..]'>s are "cross-nested" and +the embedded C<'E'> is unbalanced. (In a scalar context, this call would +return an empty string. In a void context, C<$text> would be unchanged.) + +Note that the embedded single-quotes in the string don't help in this +case, since they have not been specified as acceptable delimiters and are +therefore treated as non-delimiter characters (and ignored). + +However, if a particular species of quote character is included in the +delimiter specification, then that type of quote will be correctly handled. +for example, if C<$text> is: + + $text = 'link'; + +then + + @result = extract_bracketed( $text, '<">' ); + +returns: + + ( '', 'link', "" ) + +as expected. Without the specification of C<"> as an embedded quoter: + + @result = extract_bracketed( $text, '<>' ); + +the result would be: + + ( 'link', "" ) + +In addition to the quote delimiters C<'>, C<">, and C<`>, full Perl quote-like +quoting (i.e. q{string}, qq{string}, etc) can be specified by including the +letter 'q' as a delimiter. Hence: + + @result = extract_bracketed( $text, '' ); + +would correctly match something like this: + + $text = ''; + +See also: C<"extract_quotelike"> and C<"extract_codeblock">. + + +=head2 C + +C extracts and segments text between (balanced) +specified tags. + +The subroutine takes up to five optional arguments: + +=over 4 + +=item 1. + +A string to be processed (C<$_> if the string is omitted or C) + +=item 2. + +A string specifying a pattern to be matched as the opening tag. +If the pattern string is omitted (or C) then a pattern +that matches any standard HTML/XML tag is used. + +=item 3. + +A string specifying a pattern to be matched at the closing tag. +If the pattern string is omitted (or C) then the closing +tag is constructed by inserting a C after any leading bracket +characters in the actual opening tag that was matched (I the pattern +that matched the tag). For example, if the opening tag pattern +is specified as C<'{{\w+}}'> and actually matched the opening tag +C<"{{DATA}}">, then the constructed closing tag would be C<"{{/DATA}}">. + +=item 4. + +A string specifying a pattern to be matched as a prefix (which is to be +skipped). If omitted, optional whitespace is skipped. + +=item 5. + +A hash reference containing various parsing options (see below) + +=back + +The various options that can be specified are: + +=over 4 + +=item C $listref> + +The list reference contains one or more strings specifying patterns +that must I appear within the tagged text. + +For example, to extract +an HTML link (which should not contain nested links) use: + + extract_tagged($text, '', '', undef, {reject => ['']} ); + +=item C $listref> + +The list reference contains one or more strings specifying patterns +that are I be be treated as nested tags within the tagged text +(even if they would match the start tag pattern). + +For example, to extract an arbitrary XML tag, but ignore "empty" elements: + + extract_tagged($text, undef, undef, undef, {ignore => ['<[^>]*/>']} ); + +(also see L<"gen_delimited_pat"> below). + + +=item C $str> + +The C option indicates the action to be taken if a matching end +tag is not encountered (i.e. before the end of the string or some +C pattern matches). By default, a failure to match a closing +tag causes C to immediately fail. + +However, if the string value associated with is "MAX", then +C returns the complete text up to the point of failure. +If the string is "PARA", C returns only the first paragraph +after the tag (up to the first line that is either empty or contains +only whitespace characters). +If the string is "", the default behaviour (i.e. failure) is reinstated. + +For example, suppose the start tag "/para" introduces a paragraph, which then +continues until the next "/endpara" tag or until another "/para" tag is +encountered: + + $text = "/para line 1\n\nline 3\n/para line 4"; + + extract_tagged($text, '/para', '/endpara', undef, + {reject => '/para', fail => MAX ); + + # EXTRACTED: "/para line 1\n\nline 3\n" + +Suppose instead, that if no matching "/endpara" tag is found, the "/para" +tag refers only to the immediately following paragraph: + + $text = "/para line 1\n\nline 3\n/para line 4"; + + extract_tagged($text, '/para', '/endpara', undef, + {reject => '/para', fail => MAX ); + + # EXTRACTED: "/para line 1\n" + +Note that the specified C behaviour applies to nested tags as well. + +=back + +On success in a list context, an array of 6 elements is returned. The elements are: + +=over 4 + +=item [0] + +the extracted tagged substring (including the outermost tags), + +=item [1] + +the remainder of the input text, + +=item [2] + +the prefix substring (if any), + +=item [3] + +the opening tag + +=item [4] + +the text between the opening and closing tags + +=item [5] + +the closing tag (or "" if no closing tag was found) + +=back + +On failure, all of these values (except the remaining text) are C. + +In a scalar context, C returns just the complete +substring that matched a tagged text (including the start and end +tags). C is returned on failure. In addition, the original input +text has the returned substring (and any prefix) removed from it. + +In a void context, the input text just has the matched substring (and +any specified prefix) removed. + + +=head2 C + +(Note: This subroutine is only available under Perl5.005) + +C generates a new anonymous subroutine which +extracts text between (balanced) specified tags. In other words, +it generates a function identical in function to C. + +The difference between C and the anonymous +subroutines generated by +C, is that those generated subroutines: + +=over 4 + +=item * + +do not have to reparse tag specification or parsing options every time +they are called (whereas C has to effectively rebuild +its tag parser on every call); + +=item * + +make use of the new qr// construct to pre-compile the regexes they use +(whereas C uses standard string variable interpolation +to create tag-matching patterns). + +=back + +The subroutine takes up to four optional arguments (the same set as +C except for the string to be processed). It returns +a reference to a subroutine which in turn takes a single argument (the text to +be extracted from). + +In other words, the implementation of C is exactly +equivalent to: + + sub extract_tagged + { + my $text = shift; + $extractor = gen_extract_tagged(@_); + return $extractor->($text); + } + +(although C is not currently implemented that way, in order +to preserve pre-5.005 compatibility). + +Using C to create extraction functions for specific tags +is a good idea if those functions are going to be called more than once, since +their performance is typically twice as good as the more general-purpose +C. + + +=head2 C + +C attempts to recognize, extract, and segment any +one of the various Perl quotes and quotelike operators (see +L) Nested backslashed delimiters, embedded balanced bracket +delimiters (for the quotelike operators), and trailing modifiers are +all caught. For example, in: + + extract_quotelike 'q # an octothorpe: \# (not the end of the q!) #' + + extract_quotelike ' "You said, \"Use sed\"." ' + + extract_quotelike ' s{([A-Z]{1,8}\.[A-Z]{3})} /\L$1\E/; ' + + extract_quotelike ' tr/\\\/\\\\/\\\//ds; ' + +the full Perl quotelike operations are all extracted correctly. + +Note too that, when using the /x modifier on a regex, any comment +containing the current pattern delimiter will cause the regex to be +immediately terminated. In other words: + + 'm / + (?i) # CASE INSENSITIVE + [a-z_] # LEADING ALPHABETIC/UNDERSCORE + [a-z0-9]* # FOLLOWED BY ANY NUMBER OF ALPHANUMERICS + /x' + +will be extracted as if it were: + + 'm / + (?i) # CASE INSENSITIVE + [a-z_] # LEADING ALPHABETIC/' + +This behaviour is identical to that of the actual compiler. + +C takes two arguments: the text to be processed and +a prefix to be matched at the very beginning of the text. If no prefix +is specified, optional whitespace is the default. If no text is given, +C<$_> is used. + +In a list context, an array of 11 elements is returned. The elements are: + +=over 4 + +=item [0] + +the extracted quotelike substring (including trailing modifiers), + +=item [1] + +the remainder of the input text, + +=item [2] + +the prefix substring (if any), + +=item [3] + +the name of the quotelike operator (if any), + +=item [4] + +the left delimiter of the first block of the operation, + +=item [5] + +the text of the first block of the operation +(that is, the contents of +a quote, the regex of a match or substitution or the target list of a +translation), + +=item [6] + +the right delimiter of the first block of the operation, + +=item [7] + +the left delimiter of the second block of the operation +(that is, if it is an C, C, or C), + +=item [8] + +the text of the second block of the operation +(that is, the replacement of a substitution or the translation list +of a translation), + +=item [9] + +the right delimiter of the second block of the operation (if any), + +=item [10] + +the trailing modifiers on the operation (if any). + +=back + +For each of the fields marked "(if any)" the default value on success is +an empty string. +On failure, all of these values (except the remaining text) are C. + + +In a scalar context, C returns just the complete substring +that matched a quotelike operation (or C on failure). In a scalar or +void context, the input text has the same substring (and any specified +prefix) removed. + +Examples: + + # Remove the first quotelike literal that appears in text + + $quotelike = extract_quotelike($text,'.*?'); + + # Replace one or more leading whitespace-separated quotelike + # literals in $_ with "" + + do { $_ = join '', (extract_quotelike)[2,1] } until $@; + + + # Isolate the search pattern in a quotelike operation from $text + + ($op,$pat) = (extract_quotelike $text)[3,5]; + if ($op =~ /[ms]/) + { + print "search pattern: $pat\n"; + } + else + { + print "$op is not a pattern matching operation\n"; + } + + +=head2 C and "here documents" + +C can successfully extract "here documents" from an input +string, but with an important caveat in list contexts. + +Unlike other types of quote-like literals, a here document is rarely +a contiguous substring. For example, a typical piece of code using +here document might look like this: + + <<'EOMSG' || die; + This is the message. + EOMSG + exit; + +Given this as an input string in a scalar context, C +would correctly return the string "<<'EOMSG'\nThis is the message.\nEOMSG", +leaving the string " || die;\nexit;" in the original variable. In other words, +the two separate pieces of the here document are successfully extracted and +concatenated. + +In a list context, C would return the list + +=over 4 + +=item [0] + +"<<'EOMSG'\nThis is the message.\nEOMSG\n" (i.e. the full extracted here document, +including fore and aft delimiters), + +=item [1] + +" || die;\nexit;" (i.e. the remainder of the input text, concatenated), + +=item [2] + +"" (i.e. the prefix substring -- trivial in this case), + +=item [3] + +"<<" (i.e. the "name" of the quotelike operator) + +=item [4] + +"'EOMSG'" (i.e. the left delimiter of the here document, including any quotes), + +=item [5] + +"This is the message.\n" (i.e. the text of the here document), + +=item [6] + +"EOMSG" (i.e. the right delimiter of the here document), + +=item [7..10] + +"" (a here document has no second left delimiter, second text, second right +delimiter, or trailing modifiers). + +=back + +However, the matching position of the input variable would be set to +"exit;" (i.e. I the closing delimiter of the here document), +which would cause the earlier " || die;\nexit;" to be skipped in any +sequence of code fragment extractions. + +To avoid this problem, when it encounters a here document while +extracting from a modifiable string, C silently +rearranges the string to an equivalent piece of Perl: + + <<'EOMSG' + This is the message. + EOMSG + || die; + exit; + +in which the here document I contiguous. It still leaves the +matching position after the here document, but now the rest of the line +on which the here document starts is not skipped. + +To prevent from mucking about with the input in this way +(this is the only case where a list-context C does so), +you can pass the input variable as an interpolated literal: + + $quotelike = extract_quotelike("$var"); + + +=head2 C + +C attempts to recognize and extract a balanced +bracket delimited substring that may contain unbalanced brackets +inside Perl quotes or quotelike operations. That is, C +is like a combination of C<"extract_bracketed"> and +C<"extract_quotelike">. + +C takes the same initial three parameters as C: +a text to process, a set of delimiter brackets to look for, and a prefix to +match first. It also takes an optional fourth parameter, which allows the +outermost delimiter brackets to be specified separately (see below). + +Omitting the first argument (input text) means process C<$_> instead. +Omitting the second argument (delimiter brackets) indicates that only C<'{'> is to be used. +Omitting the third argument (prefix argument) implies optional whitespace at the start. +Omitting the fourth argument (outermost delimiter brackets) indicates that the +value of the second argument is to be used for the outermost delimiters. + +Once the prefix an the outermost opening delimiter bracket have been +recognized, code blocks are extracted by stepping through the input text and +trying the following alternatives in sequence: + +=over 4 + +=item 1. + +Try and match a closing delimiter bracket. If the bracket was the same +species as the last opening bracket, return the substring to that +point. If the bracket was mismatched, return an error. + +=item 2. + +Try to match a quote or quotelike operator. If found, call +C to eat it. If C fails, return +the error it returned. Otherwise go back to step 1. + +=item 3. + +Try to match an opening delimiter bracket. If found, call +C recursively to eat the embedded block. If the +recursive call fails, return an error. Otherwise, go back to step 1. + +=item 4. + +Unconditionally match a bareword or any other single character, and +then go back to step 1. + +=back + + +Examples: + + # Find a while loop in the text + + if ($text =~ s/.*?while\s*\{/{/) + { + $loop = "while " . extract_codeblock($text); + } + + # Remove the first round-bracketed list (which may include + # round- or curly-bracketed code blocks or quotelike operators) + + extract_codeblock $text, "(){}", '[^(]*'; + + +The ability to specify a different outermost delimiter bracket is useful +in some circumstances. For example, in the Parse::RecDescent module, +parser actions which are to be performed only on a successful parse +are specified using a Cdefer:...E> directive. For example: + + sentence: subject verb object + + +Parse::RecDescent uses CE')> to extract the code +within the Cdefer:...E> directive, but there's a problem. + +A deferred action like this: + + 10) {$count--}} > + +will be incorrectly parsed as: + + + +because the "less than" operator is interpreted as a closing delimiter. + +But, by extracting the directive using +SE')>> +the '>' character is only treated as a delimited at the outermost +level of the code block, so the directive is parsed correctly. + +=head2 C + +The C subroutine takes a string to be processed and a +list of extractors (subroutines or regular expressions) to apply to that string. + +In an array context C returns an array of substrings +of the original string, as extracted by the specified extractors. +In a scalar context, C returns the first +substring successfully extracted from the original string. In both +scalar and void contexts the original string has the first successfully +extracted substring removed from it. In all contexts +C starts at the current C of the string, and +sets that C appropriately after it matches. + +Hence, the aim of a call to C in a list context +is to split the processed string into as many non-overlapping fields as +possible, by repeatedly applying each of the specified extractors +to the remainder of the string. Thus C is +a generalized form of Perl's C subroutine. + +The subroutine takes up to four optional arguments: + +=over 4 + +=item 1. + +A string to be processed (C<$_> if the string is omitted or C) + +=item 2. + +A reference to a list of subroutine references and/or qr// objects and/or +literal strings and/or hash references, specifying the extractors +to be used to split the string. If this argument is omitted (or +C) the list: + + [ + sub { extract_variable($_[0], '') }, + sub { extract_quotelike($_[0],'') }, + sub { extract_codeblock($_[0],'{}','') }, + ] + +is used. + + +=item 3. + +A number specifying the maximum number of fields to return. If this +argument is omitted (or C), split continues as long as possible. + +If the third argument is I, then extraction continues until I fields +have been successfully extracted, or until the string has been completely +processed. + +Note that in scalar and void contexts the value of this argument is +automatically reset to 1 (under C<-w>, a warning is issued if the argument +has to be reset). + +=item 4. + +A value indicating whether unmatched substrings (see below) within the +text should be skipped or returned as fields. If the value is true, +such substrings are skipped. Otherwise, they are returned. + +=back + +The extraction process works by applying each extractor in +sequence to the text string. + +If the extractor is a subroutine it is called in a list context and is +expected to return a list of a single element, namely the extracted +text. It may optionally also return two further arguments: a string +representing the text left after extraction (like $' for a pattern +match), and a string representing any prefix skipped before the +extraction (like $` in a pattern match). Note that this is designed +to facilitate the use of other Text::Balanced subroutines with +C. Note too that the value returned by an extractor +subroutine need not bear any relationship to the corresponding substring +of the original text (see examples below). + +If the extractor is a precompiled regular expression or a string, +it is matched against the text in a scalar context with a leading +'\G' and the gc modifiers enabled. The extracted value is either +$1 if that variable is defined after the match, or else the +complete match (i.e. $&). + +If the extractor is a hash reference, it must contain exactly one element. +The value of that element is one of the +above extractor types (subroutine reference, regular expression, or string). +The key of that element is the name of a class into which the successful +return value of the extractor will be blessed. + +If an extractor returns a defined value, that value is immediately +treated as the next extracted field and pushed onto the list of fields. +If the extractor was specified in a hash reference, the field is also +blessed into the appropriate class, + +If the extractor fails to match (in the case of a regex extractor), or returns an empty list or an undefined value (in the case of a subroutine extractor), it is +assumed to have failed to extract. +If none of the extractor subroutines succeeds, then one +character is extracted from the start of the text and the extraction +subroutines reapplied. Characters which are thus removed are accumulated and +eventually become the next field (unless the fourth argument is true, in which +case they are discarded). + +For example, the following extracts substrings that are valid Perl variables: + + @fields = extract_multiple($text, + [ sub { extract_variable($_[0]) } ], + undef, 1); + +This example separates a text into fields which are quote delimited, +curly bracketed, and anything else. The delimited and bracketed +parts are also blessed to identify them (the "anything else" is unblessed): + + @fields = extract_multiple($text, + [ + { Delim => sub { extract_delimited($_[0],q{'"}) } }, + { Brack => sub { extract_bracketed($_[0],'{}') } }, + ]); + +This call extracts the next single substring that is a valid Perl quotelike +operator (and removes it from $text): + + $quotelike = extract_multiple($text, + [ + sub { extract_quotelike($_[0]) }, + ], undef, 1); + +Finally, here is yet another way to do comma-separated value parsing: + + @fields = extract_multiple($csv_text, + [ + sub { extract_delimited($_[0],q{'"}) }, + qr/([^,]+)(.*)/, + ], + undef,1); + +The list in the second argument means: +I<"Try and extract a ' or " delimited string, otherwise extract anything up to a comma...">. +The undef third argument means: +I<"...as many times as possible...">, +and the true value in the fourth argument means +I<"...discarding anything else that appears (i.e. the commas)">. + +If you wanted the commas preserved as separate fields (i.e. like split +does if your split pattern has capturing parentheses), you would +just make the last parameter undefined (or remove it). + + +=head2 C + +The C subroutine takes a single (string) argument and + > builds a Friedl-style optimized regex that matches a string delimited +by any one of the characters in the single argument. For example: + + gen_delimited_pat(q{'"}) + +returns the regex: + + (?:\"(?:\\\"|(?!\").)*\"|\'(?:\\\'|(?!\').)*\') + +Note that the specified delimiters are automatically quotemeta'd. + +A typical use of C would be to build special purpose tags +for C. For example, to properly ignore "empty" XML elements +(which might contain quoted strings): + + my $empty_tag = '<(' . gen_delimited_pat(q{'"}) . '|.)+/>'; + + extract_tagged($text, undef, undef, undef, {ignore => [$empty_tag]} ); + + +C may also be called with an optional second argument, +which specifies the "escape" character(s) to be used for each delimiter. +For example to match a Pascal-style string (where ' is the delimiter +and '' is a literal ' within the string): + + gen_delimited_pat(q{'},q{'}); + +Different escape characters can be specified for different delimiters. +For example, to specify that '/' is the escape for single quotes +and '%' is the escape for double quotes: + + gen_delimited_pat(q{'"},q{/%}); + +If more delimiters than escape chars are specified, the last escape char +is used for the remaining delimiters. +If no escape char is specified for a given specified delimiter, '\' is used. + +Note that +C was previously called +C. That name may still be used, but is now deprecated. + + +=head1 DIAGNOSTICS + +In a list context, all the functions return C<(undef,$original_text)> +on failure. In a scalar context, failure is indicated by returning C +(in this case the input text is not modified in any way). + +In addition, on failure in I context, the C<$@> variable is set. +Accessing C<$@-E{error}> returns one of the error diagnostics listed +below. +Accessing C<$@-E{pos}> returns the offset into the original string at +which the error was detected (although not necessarily where it occurred!) +Printing C<$@> directly produces the error message, with the offset appended. +On success, the C<$@> variable is guaranteed to be C. + +The available diagnostics are: + +=over 4 + +=item C + +The delimiter provided to C was not one of +C<'()[]EE{}'>. + +=item C + +A non-optional prefix was specified but wasn't found at the start of the text. + +=item C + +C or C was expecting a +particular kind of bracket at the start of the text, and didn't find it. + +=item C + +C didn't find one of the quotelike operators C, +C, C, C, C, C or C at the start of the substring +it was extracting. + +=item C + +C, C or C encountered +a closing bracket where none was expected. + +=item C + +C, C or C ran +out of characters in the text before closing one or more levels of nested +brackets. + +=item C + +C attempted to match an embedded quoted substring, but +failed to find a closing quote to match it. + +=item C + +C was unable to find a closing delimiter to match the +one that opened the quote-like operation. + +=item C + +C, C or C found +a valid bracket delimiter, but it was the wrong species. This usually +indicates a nesting error, but may indicate incorrect quoting or escaping. + +=item C + +C or C found one of the +quotelike operators C, C, C, C, C, C or C +without a suitable block after it. + +=item C + +C was expecting one of '$', '@', or '%' at the start of +a variable, but didn't find any of them. + +=item C + +C found a '$', '@', or '%' indicating a variable, but that +character was not followed by a legal Perl identifier. + +=item C + +C failed to find any of the outermost opening brackets +that were specified. + +=item C + +A nested code block was found that started with a delimiter that was specified +as being only to be used as an outermost bracket. + +=item C + +C or C found one of the +quotelike operators C, C or C followed by only one block. + +=item C + +C failed to find a closing bracket to match the outermost +opening bracket. + +=item C + +C did not find a suitable opening tag (after any specified +prefix was removed). + +=item C + +C matched the specified opening tag and tried to +modify the matched text to produce a matching closing tag (because +none was specified). It failed to generate the closing tag, almost +certainly because the opening tag did not start with a +bracket of some kind. + +=item C + +C found a nested tag that appeared in the "reject" list +(and the failure mode was not "MAX" or "PARA"). + +=item C + +C found a nested opening tag that was not matched by a +corresponding nested closing tag (and the failure mode was not "MAX" or "PARA"). + +=item C + +C reached the end of the text without finding a closing tag +to match the original opening tag (and the failure mode was not +"MAX" or "PARA"). + + + + +=back + + +=head1 AUTHOR + +Damian Conway (damian@conway.org) + + +=head1 BUGS AND IRRITATIONS + +There are undoubtedly serious bugs lurking somewhere in this code, if +only because parts of it give the impression of understanding a great deal +more about Perl than they really do. + +Bug reports and other feedback are most welcome. + + +=head1 COPYRIGHT + + Copyright (c) 1997-2001, Damian Conway. All Rights Reserved. + This module is free software. It may be used, redistributed + and/or modified under the same terms as Perl itself. diff --git a/testsuite/input-files/perllib/Text/Balanced/Changes b/testsuite/input-files/perllib/Text/Balanced/Changes new file mode 100644 index 00000000..2b42f944 --- /dev/null +++ b/testsuite/input-files/perllib/Text/Balanced/Changes @@ -0,0 +1,263 @@ +Revision history for Perl extension Text::Balanced. + +1.00 Mon Aug 11 12:42:56 1997 + + - original version + + +1.01 Mon Sep 8 18:09:18 EST 1997 + + - changed "quotemeta" to "quotemeta $_" to work + around bug in Perl 5.002 and 5.003 + + +1.10 Tue Sep 30 17:23:23 EST 1997 + + - reworked extract_quotelike to correct handling of some obscure cases + + +1.21 Sat Oct 4 17:21:54 EST 1997 + + - synchronised with Parse::RecDescent distribution (version number + will now reflect that package) + +1.23 Fri Oct 17 10:26:38 EST 1997 + + - changed behaviour in scalar and void contexts. Scalar contexts + now return only the extracted string. Void contexts now remove + the extracted string from the first argument (or $_). + +1.24 + + - changed behaviour in scalar contexts. Scalar contexts + now return the extracted string _and_ remove it from the + first argument (or $_). + + - changed return values on failure (all contexts return undef + for invalid return fields) + + - fixed some lurking bugs with trailing modifier handling + + - added :ALL tag to simplify wholesale importing of functions + + - fixed serious bug with embedded division operators ("/") + This now also allows the ?...? form of pattern matching! + +1.35 Wed Jun 24 09:53:31 1998 + + - fixed handling of :: quantifiers in extract_variable() + + - numerous trivial lexical changes to make xemacs happy + + +1.36 Tue Jul 14 12:26:04 1998 + + - Reinstated POD file missing from previous distribution + + - Added undocumented fourth parameter to extract_codeblock + so as to correctly handle (?) and (s?) modifiers in + RecDescent grammars. + + +1.40 Tue Aug 4 13:54:52 1998 + + - Added (optional) handling of embedded quoted text to + extract_delimited (see revised entry in Balanced.pod) + + - Added extract_tagged which extracts text between arbitrary, + optionally nested start and end tags (see new entry in + Balanced.pod). + + - Added delimited_pat which builds a pattern which matches a + string delimited by any of the delimiters specified (see new + entry in Balanced.pod). + + - Added test.pl + + +1.41 Mon Aug 10 14:51:50 1998 + + - Reinstated change to extract_codeblock from 1.36 which were + mysteriously lost in 1.40 + + +1.50 Thu Aug 27 09:20:19 1998 + + - Improved the structure of the regex generated by + delimited_pat (and used in extract_delimited). It's + considerably more complex, but also more robust and + much faster in the worst case. + + - Altered extract_variable to accept whitespace in variables, + e.g. '$ a -> {'b'} -> [2]' + + + +1.51 Sat Feb 13 10:31:55 1999 + + - Fixed bugs in prefix matching in extract_variable: + * incorrectly used default if '' specified + * now handles $#array correctly + + - Fixed bugs in extract_codeblock: + * Now handles !~ properly + * Now handles embedded comments better. + * Now handles "raw" pattern matches better. + + - Added support for single strings or qr's as + 'reject' and 'ignore' args to extract_tagged() + + - Added gen_extract_tagged() to "precompile" + a specific tag extractor for repeated use + (approximately 3 times faster!) + + +1.52 Thu Mar 4 12:43:38 1999 + + - Added CSV parsing example to documentation of extract_multiple. + + - Fixed a bug with extract_codeblock in "RecDescent" mode + (it would accept "subrule(s?)" and "subrule(?)", but + not "subrule(s)"). Thanks, Jan. + + +1.66 Fri Jul 2 13:29:22 1999 + + - Added ability to use quotelike operators in extract_bracketed + + - Fixed bug under 5.003 ('foreach my $func' not understood) + + - Added escape specification as fourth arg to &extract_delimited + + - Fixed handling of &delimited_pat and &extract_delimited + when delimiter is same as escape + + - Fixed handling of ->, =>, and >> in &extract_code + when delimiters are "<>" + + +1.76 Fri Nov 19 06:51:54 1999 + + - IMPORTANT: Now requires 5.005 or better. + + - IMPORTANT: Made extract methods sensitive to the pos() + value of the text they are parsing. In other words, + all extract subroutines now act like patterns of the form + /\G.../gc. See documentation for details. + + - IMPORTANT: Changed semantics of extract_multiple, in line + with the above change, and to simplify the semantics to + something vaguely predictable. See documentation for details. + + - Added ability to use qr/../'s and raw strings as extractors + in extract_multiple. See documentation. + + - Added fourth argument to extract_codeblock to allow + outermost brackets to be separately specified. See + documentation for details. + + - Reimplemented internals of all extraction subroutines + for significant speed-ups (between 100% and 2000% + improvement). + + - Fixed nasty bug in extract_variable and extract_codeblock + (they were returning prefix as well in scalar context) + + - Allowed read-only strings to be used as arguments in + scalar contexts. + + - Renamed delimited_pat to gen-delimited pat (in line with + gen_extract_tagged). Old name still works, but is now deprecated. + + - Tweaked all extraction subs so they correctly handle + zero-length prefix matches after another zero-length match. + + +1.77 Mon Nov 22 06:08:23 1999 + + - Fixed major bug in extract_codeblock (would not + terminate if there was trailing whitespace) + + - Improved /.../ pattern parsing within codeblocks + + +1.81 Wed Sep 13 11:58:49 2000 + + - Fixed test count in extract_codeblock.t + + - Fixed improbable bug with trailing ->'s in extract_variable + + - Fixed (HT|X)ML tag extraction in extract_tagged (thanks, Tim) + + - Added explanatory note about prefix matching (thanks again, Tim) + + - Added handling of globs and sub refs to extract_variable + + - Pod tweak (thanks Abigail) + + - Allowed right tags to be run-time evaluated, so + extract_tagged($text, '/([a-z]+)', '/end$1') works + as expected. + + - Added optional blessing of matches via extract_multiple + + - Fixed bug in autogeneration of closing tags in extract_tagged + (Thanks, Coke) + + - Fixed bug in interaction between extract_multiple and + gen_extract_tagged (Thanks Anthony) + + +1.82 Sun Jan 14 16:56:04 2001 + + - Fixed nit in extract_variable.t + (tested more cases than it promised to) + + - Fixed bug extracting prefix in extract_quotelike (Thanks Michael) + + - Added handling of Perl 4 package qualifier: $Package'var, etc. + + - Added handling of here docs (see documentation for limitations) + + - Added reporting of failure position via $@->{pos} (see documentation) + + +1.83 Mon Jan 15 12:43:12 2001 + + - Fixed numerous bugs in here doc extraction (many thanks Tim) + + +1.84 Thu Apr 26 11:58:13 2001 + + - Fixed bug in certain extractions not matching strings + with embedded newlines (thanks Robin) + + +1.85 Sun Jun 3 07:47:18 2001 + + - Fixed bug in extract_variable recognizing method calls that + start with an underscore (thanks Jeff) + + +1.86 Mon Sep 3 06:57:08 2001 + + - Revised licence for inclusion in core distribution + + - Consolidated POD in .pm file + + - renamed tests to let DOS cope with them + + +1.87 Thu Nov 15 21:25:35 2001 + + - Made extract_multiple aware of skipped prefixes returned + by subroutine extractors (such as extract_quotelike, etc.) + + - Made extract_variable aware of punctuation variables + + - Corified tests + + +1.89 Sun Nov 18 22:49:50 2001 + + - Fixed extvar.t tests diff --git a/testsuite/input-files/perllib/Text/Balanced/README b/testsuite/input-files/perllib/Text/Balanced/README new file mode 100644 index 00000000..ef2f376f --- /dev/null +++ b/testsuite/input-files/perllib/Text/Balanced/README @@ -0,0 +1,84 @@ +============================================================================== + Release of version 1.89 of Text::Balanced +============================================================================== + + +NAME + + Text::Balanced - Extract delimited text sequences from strings. + + +SUMMARY (see Balanced.pod for full details) + + Text::Balanced::extract_delimited + + `extract_delimited' extracts the initial substring of a string + which is delimited by a user-specified set of single-character + delimiters, whilst ignoring any backslash-escaped delimiter + characters. + + Text::Balanced::extract_bracketed + + `extract_bracketed' extracts a balanced-bracket-delimited substring + (using any one (or more) of the user-specified delimiter brackets: + '(..)', '{..}', '[..]', or '<..>'). + + Text::Balanced::extract_quotelike + + `extract_quotelike' attempts to recognize and extract any one of the + various Perl quote and quotelike operators (see "perlop(3)"). Embedded + backslashed delimiters, nested bracket delimiters (for the + quotelike operators), and trailing modifiers are all correctly handled. + + Text::Balanced::extract_codeblock + + `extract_codeblock' attempts to recognize and extract a + balanced bracket-delimited substring which may also contain + unbalanced brackets inside Perl quotes or quotelike + operations. That is, `extract_codeblock' is like a combination + of `extract_bracketed' and `extract_quotelike'. + + Text::Balanced::extract_tagged + + `extract_tagged' attempts to recognize and extract a + substring between two arbitrary "tag" patterns (a start tag + and an end tag). + + +INSTALLATION + + It's all pure Perl, so just put the .pm file in its appropriate + local Perl subdirectory. + + +AUTHOR + + Damian Conway (damian@cs.monash.edu.au) + + +COPYRIGHT + + Copyright (c) 1997-2001, Damian Conway. All Rights Reserved. + This module is free software. It may be used, redistributed + and/or modified under the same terms as Perl itself. + + + +============================================================================== + +CHANGES IN VERSION 1.89 + + + - Fixed extvar.t tests + + +============================================================================== + +AVAILABILITY + +Text::Balanced has been uploaded to the CPAN +and is also available from: + + http://www.csse.monash.edu.au/~damian/CPAN/Text-Balanced.tar.gz + +============================================================================== diff --git a/testsuite/input-files/perllib/Text/ParseWords.pm b/testsuite/input-files/perllib/Text/ParseWords.pm new file mode 100644 index 00000000..6949c459 --- /dev/null +++ b/testsuite/input-files/perllib/Text/ParseWords.pm @@ -0,0 +1,263 @@ +package Text::ParseWords; + +use vars qw($VERSION @ISA @EXPORT $PERL_SINGLE_QUOTE); +$VERSION = "3.21"; + +require 5.000; + +use Exporter; +@ISA = qw(Exporter); +@EXPORT = qw(shellwords quotewords nested_quotewords parse_line); +@EXPORT_OK = qw(old_shellwords); + + +sub shellwords { + local(@lines) = @_; + $lines[$#lines] =~ s/\s+$//; + return(quotewords('\s+', 0, @lines)); +} + + + +sub quotewords { + my($delim, $keep, @lines) = @_; + my($line, @words, @allwords); + + + foreach $line (@lines) { + @words = parse_line($delim, $keep, $line); + return() unless (@words || !length($line)); + push(@allwords, @words); + } + return(@allwords); +} + + + +sub nested_quotewords { + my($delim, $keep, @lines) = @_; + my($i, @allwords); + + for ($i = 0; $i < @lines; $i++) { + @{$allwords[$i]} = parse_line($delim, $keep, $lines[$i]); + return() unless (@{$allwords[$i]} || !length($lines[$i])); + } + return(@allwords); +} + + + +sub parse_line { + # We will be testing undef strings + no warnings; + use re 'taint'; # if it's tainted, leave it as such + + my($delimiter, $keep, $line) = @_; + my($quote, $quoted, $unquoted, $delim, $word, @pieces); + + while (length($line)) { + + ($quote, $quoted, undef, $unquoted, $delim, undef) = + $line =~ m/^(["']) # a $quote + ((?:\\.|(?!\1)[^\\])*) # and $quoted text + \1 # followed by the same quote + ([\000-\377]*) # and the rest + | # --OR-- + ^((?:\\.|[^\\"'])*?) # an $unquoted text + (\Z(?!\n)|(?-x:$delimiter)|(?!^)(?=["'])) + # plus EOL, delimiter, or quote + ([\000-\377]*) # the rest + /x; # extended layout + return() unless( $quote || length($unquoted) || length($delim)); + + $line = $+; + + if ($keep) { + $quoted = "$quote$quoted$quote"; + } + else { + $unquoted =~ s/\\(.)/$1/g; + if (defined $quote) { + $quoted =~ s/\\(.)/$1/g if ($quote eq '"'); + $quoted =~ s/\\([\\'])/$1/g if ( $PERL_SINGLE_QUOTE && $quote eq "'"); + } + } + $word .= defined $quote ? $quoted : $unquoted; + + if (length($delim)) { + push(@pieces, $word); + push(@pieces, $delim) if ($keep eq 'delimiters'); + undef $word; + } + if (!length($line)) { + push(@pieces, $word); + } + } + return(@pieces); +} + + + +sub old_shellwords { + + # Usage: + # use ParseWords; + # @words = old_shellwords($line); + # or + # @words = old_shellwords(@lines); + + local($_) = join('', @_); + my(@words,$snippet,$field); + + s/^\s+//; + while ($_ ne '') { + $field = ''; + for (;;) { + if (s/^"(([^"\\]|\\.)*)"//) { + ($snippet = $1) =~ s#\\(.)#$1#g; + } + elsif (/^"/) { + return(); + } + elsif (s/^'(([^'\\]|\\.)*)'//) { + ($snippet = $1) =~ s#\\(.)#$1#g; + } + elsif (/^'/) { + return(); + } + elsif (s/^\\(.)//) { + $snippet = $1; + } + elsif (s/^([^\s\\'"]+)//) { + $snippet = $1; + } + else { + s/^\s+//; + last; + } + $field .= $snippet; + } + push(@words, $field); + } + @words; +} + +1; + +__END__ + +=head1 NAME + +Text::ParseWords - parse text into an array of tokens or array of arrays + +=head1 SYNOPSIS + + use Text::ParseWords; + @lists = &nested_quotewords($delim, $keep, @lines); + @words = "ewords($delim, $keep, @lines); + @words = &shellwords(@lines); + @words = &parse_line($delim, $keep, $line); + @words = &old_shellwords(@lines); # DEPRECATED! + +=head1 DESCRIPTION + +The &nested_quotewords() and "ewords() functions accept a delimiter +(which can be a regular expression) +and a list of lines and then breaks those lines up into a list of +words ignoring delimiters that appear inside quotes. "ewords() +returns all of the tokens in a single long list, while &nested_quotewords() +returns a list of token lists corresponding to the elements of @lines. +&parse_line() does tokenizing on a single string. The &*quotewords() +functions simply call &parse_lines(), so if you're only splitting +one line you can call &parse_lines() directly and save a function +call. + +The $keep argument is a boolean flag. If true, then the tokens are +split on the specified delimiter, but all other characters (quotes, +backslashes, etc.) are kept in the tokens. If $keep is false then the +&*quotewords() functions remove all quotes and backslashes that are +not themselves backslash-escaped or inside of single quotes (i.e., +"ewords() tries to interpret these characters just like the Bourne +shell). NB: these semantics are significantly different from the +original version of this module shipped with Perl 5.000 through 5.004. +As an additional feature, $keep may be the keyword "delimiters" which +causes the functions to preserve the delimiters in each string as +tokens in the token lists, in addition to preserving quote and +backslash characters. + +&shellwords() is written as a special case of "ewords(), and it +does token parsing with whitespace as a delimiter-- similar to most +Unix shells. + +=head1 EXAMPLES + +The sample program: + + use Text::ParseWords; + @words = "ewords('\s+', 0, q{this is "a test" of\ quotewords \"for you}); + $i = 0; + foreach (@words) { + print "$i: <$_>\n"; + $i++; + } + +produces: + + 0: + 1: + 2: + 3: + 4: <"for> + 5: + +demonstrating: + +=over 4 + +=item 0 + +a simple word + +=item 1 + +multiple spaces are skipped because of our $delim + +=item 2 + +use of quotes to include a space in a word + +=item 3 + +use of a backslash to include a space in a word + +=item 4 + +use of a backslash to remove the special meaning of a double-quote + +=item 5 + +another simple word (note the lack of effect of the +backslashed double-quote) + +=back + +Replacing C<"ewords('\s+', 0, q{this is...})> +with C<&shellwords(q{this is...})> +is a simpler way to accomplish the same thing. + +=head1 AUTHORS + +Maintainer is Hal Pomeranz , 1994-1997 (Original +author unknown). Much of the code for &parse_line() (including the +primary regexp) from Joerk Behrends . + +Examples section another documentation provided by John Heidemann + + +Bug reports, patches, and nagging provided by lots of folks-- thanks +everybody! Special thanks to Michael Schwern +for assuring me that a &nested_quotewords() would be useful, and to +Jeff Friedl for telling me not to worry about +error-checking (sort of-- you had to be there). + +=cut diff --git a/testsuite/input-files/perllib/Text/Soundex.pm b/testsuite/input-files/perllib/Text/Soundex.pm new file mode 100644 index 00000000..64a9e650 --- /dev/null +++ b/testsuite/input-files/perllib/Text/Soundex.pm @@ -0,0 +1,150 @@ +package Text::Soundex; +require 5.000; +require Exporter; + +@ISA = qw(Exporter); +@EXPORT = qw(&soundex $soundex_nocode); + +$VERSION = '1.01'; + +# $Id: soundex.pl,v 1.2 1994/03/24 00:30:27 mike Exp $ +# +# Implementation of soundex algorithm as described by Knuth in volume +# 3 of The Art of Computer Programming, with ideas stolen from Ian +# Phillipps . +# +# Mike Stok , 2 March 1994. +# +# Knuth's test cases are: +# +# Euler, Ellery -> E460 +# Gauss, Ghosh -> G200 +# Hilbert, Heilbronn -> H416 +# Knuth, Kant -> K530 +# Lloyd, Ladd -> L300 +# Lukasiewicz, Lissajous -> L222 +# +# $Log: soundex.pl,v $ +# Revision 1.2 1994/03/24 00:30:27 mike +# Subtle bug (any excuse :-) spotted by Rich Pinder +# in the way I handles leasing characters which were different but had +# the same soundex code. This showed up comparing it with Oracle's +# soundex output. +# +# Revision 1.1 1994/03/02 13:01:30 mike +# Initial revision +# +# +############################################################################## + +# $soundex_nocode is used to indicate a string doesn't have a soundex +# code, I like undef other people may want to set it to 'Z000'. + +$soundex_nocode = undef; + +sub soundex +{ + local (@s, $f, $fc, $_) = @_; + + push @s, '' unless @s; # handle no args as a single empty string + + foreach (@s) + { + $_ = uc $_; + tr/A-Z//cd; + + if ($_ eq '') + { + $_ = $soundex_nocode; + } + else + { + ($f) = /^(.)/; + tr/AEHIOUWYBFPVCGJKQSXZDTLMNR/00000000111122222222334556/; + ($fc) = /^(.)/; + s/^$fc+//; + tr///cs; + tr/0//d; + $_ = $f . $_ . '000'; + s/^(.{4}).*/$1/; + } + } + + wantarray ? @s : shift @s; +} + +1; + +__END__ + +=head1 NAME + +Text::Soundex - Implementation of the Soundex Algorithm as Described by Knuth + +=head1 SYNOPSIS + + use Text::Soundex; + + $code = soundex $string; # get soundex code for a string + @codes = soundex @list; # get list of codes for list of strings + + # set value to be returned for strings without soundex code + + $soundex_nocode = 'Z000'; + +=head1 DESCRIPTION + +This module implements the soundex algorithm as described by Donald Knuth +in Volume 3 of B. The algorithm is +intended to hash words (in particular surnames) into a small space using a +simple model which approximates the sound of the word when spoken by an English +speaker. Each word is reduced to a four character string, the first +character being an upper case letter and the remaining three being digits. + +If there is no soundex code representation for a string then the value of +C<$soundex_nocode> is returned. This is initially set to C, but +many people seem to prefer an I value like C +(how unlikely this is depends on the data set being dealt with.) Any value +can be assigned to C<$soundex_nocode>. + +In scalar context C returns the soundex code of its first +argument, and in list context a list is returned in which each element is the +soundex code for the corresponding argument passed to C e.g. + + @codes = soundex qw(Mike Stok); + +leaves C<@codes> containing C<('M200', 'S320')>. + +=head1 EXAMPLES + +Knuth's examples of various names and the soundex codes they map to +are listed below: + + Euler, Ellery -> E460 + Gauss, Ghosh -> G200 + Hilbert, Heilbronn -> H416 + Knuth, Kant -> K530 + Lloyd, Ladd -> L300 + Lukasiewicz, Lissajous -> L222 + +so: + + $code = soundex 'Knuth'; # $code contains 'K530' + @list = soundex qw(Lloyd Gauss); # @list contains 'L300', 'G200' + +=head1 LIMITATIONS + +As the soundex algorithm was originally used a B time ago in the US +it considers only the English alphabet and pronunciation. + +As it is mapping a large space (arbitrary length strings) onto a small +space (single letter plus 3 digits) no inference can be made about the +similarity of two strings which end up with the same soundex code. For +example, both C and C end up with a soundex code +of C. + +=head1 AUTHOR + +This code was implemented by Mike Stok (C) from the +description given by Knuth. Ian Phillipps (C) and Rich Pinder +(C) supplied ideas and spotted mistakes. diff --git a/testsuite/input-files/perllib/Text/Tabs.pm b/testsuite/input-files/perllib/Text/Tabs.pm new file mode 100644 index 00000000..b26f8f40 --- /dev/null +++ b/testsuite/input-files/perllib/Text/Tabs.pm @@ -0,0 +1,97 @@ + +package Text::Tabs; + +require Exporter; + +@ISA = (Exporter); +@EXPORT = qw(expand unexpand $tabstop); + +use vars qw($VERSION $tabstop $debug); +$VERSION = 98.112801; + +use strict; + +BEGIN { + $tabstop = 8; + $debug = 0; +} + +sub expand +{ + my (@l) = @_; + for $_ (@l) { + 1 while s/(^|\n)([^\t\n]*)(\t+)/ + $1. $2 . (" " x + ($tabstop * length($3) + - (length($2) % $tabstop))) + /sex; + } + return @l if wantarray; + return $l[0]; +} + +sub unexpand +{ + my (@l) = @_; + my @e; + my $x; + my $line; + my @lines; + my $lastbit; + for $x (@l) { + @lines = split("\n", $x, -1); + for $line (@lines) { + $line = expand($line); + @e = split(/(.{$tabstop})/,$line,-1); + $lastbit = pop(@e); + $lastbit = '' unless defined $lastbit; + $lastbit = "\t" + if $lastbit eq " "x$tabstop; + for $_ (@e) { + if ($debug) { + my $x = $_; + $x =~ s/\t/^I\t/gs; + print "sub on '$x'\n"; + } + s/ +$/\t/; + } + $line = join('',@e, $lastbit); + } + $x = join("\n", @lines); + } + return @l if wantarray; + return $l[0]; +} + +1; +__END__ + + +=head1 NAME + +Text::Tabs -- expand and unexpand tabs per the unix expand(1) and unexpand(1) + +=head1 SYNOPSIS + + use Text::Tabs; + + $tabstop = 4; + @lines_without_tabs = expand(@lines_with_tabs); + @lines_with_tabs = unexpand(@lines_without_tabs); + +=head1 DESCRIPTION + +Text::Tabs does about what the unix utilities expand(1) and unexpand(1) +do. Given a line with tabs in it, expand will replace the tabs with +the appropriate number of spaces. Given a line with or without tabs in +it, unexpand will add tabs when it can save bytes by doing so. Invisible +compression with plain ascii! + +=head1 BUGS + +expand doesn't handle newlines very quickly -- do not feed it an +entire document in one string. Instead feed it an array of lines. + +=head1 AUTHOR + +David Muir Sharnoff diff --git a/testsuite/input-files/perllib/Text/TabsWrap/CHANGELOG b/testsuite/input-files/perllib/Text/TabsWrap/CHANGELOG new file mode 100644 index 00000000..7f0720a1 --- /dev/null +++ b/testsuite/input-files/perllib/Text/TabsWrap/CHANGELOG @@ -0,0 +1,74 @@ += 2001/09/29 + +Philip Newton sent in a clean patch that +added support for defining words differently; that prevents +Text::Wrap from untainting strings; and that fixes a documentation +bug. + +So that fill.t can be used in the version included in the perl +distribution, fill.t no longer uses File::Slurp. + +Both Sweth Chandramouli and Drew Degentesh + both objected to the automatic unexpand +that Text::Wrap does on its results. Drew sent a patch which +has been integrated. + +Way back in '97, Joel Earl asked that +it be possible to use a line separator other than \n when +adding new lines. There is now support for that. + += 2001/01/30 + +Bugfix by Michael G Schwern : don't add extra +whitespace when working one an array of input (as opposed to a +single string). + +Performance rewrite: use m/\G/ rather than s///. + +You can now specify that words that are too long to wrap can simply +overflow the line. Feature requested by James Hoagland + and by John Porter . + +Documentation changes from Rich Bowen . + += 1998/11/29 + +Combined Fill.pm into Wrap.pm. It appears there are versions of +Wrap.pm with fill in them. + += 1998/11/28 + +Over the last couple of years, many people sent in various +rewrites of Text::Wrap. I should have done something about +updating it long ago. If someone wants to take it over from +me, discuss it in perl-porters. I'll be happy to hand it +over. + +Anyway, I have a bunch of people to thank. I didn't +use what any of them sent in, but I did take ideas from +all of them. Many sent in complete new implamentations. + + Ivan Brawley + + Jacqui Caren + + Jeff Kowalski + + Allen Smith + + Sullivan N. Beck + +The end result is a very slight change in the API. There +is now an additional package variable: $Text::Wrap::huge. +When $huge is set to 'die' then long words will cause +wrap() to die. When it is set to 'wrap', long words will +be wrapped. The default is 'wrap'. + +LONG WORDS WILL NOW BE WRAPPED BY DEFAULT. +This is a change in behavior. + +At the bottom of Text::Wrap, there was a function (fill()) +sitting there unpublished. There was a note that Tim Pierce +had a faster version, but a search on CPAN failed to turn it +up. Text::Fill is now available. + diff --git a/testsuite/input-files/perllib/Text/Wrap.pm b/testsuite/input-files/perllib/Text/Wrap.pm new file mode 100644 index 00000000..23276660 --- /dev/null +++ b/testsuite/input-files/perllib/Text/Wrap.pm @@ -0,0 +1,212 @@ +package Text::Wrap; + +require Exporter; + +@ISA = qw(Exporter); +@EXPORT = qw(wrap fill); +@EXPORT_OK = qw($columns $break $huge); + +$VERSION = 2001.0929; + +use vars qw($VERSION $columns $debug $break $huge $unexpand $tabstop + $separator); +use strict; + +BEGIN { + $columns = 76; # <= screen width + $debug = 0; + $break = '\s'; + $huge = 'wrap'; # alternatively: 'die' or 'overflow' + $unexpand = 1; + $tabstop = 8; + $separator = "\n"; +} + +use Text::Tabs qw(expand unexpand); + +sub wrap +{ + my ($ip, $xp, @t) = @_; + + local($Text::Tabs::tabstop) = $tabstop; + my $r = ""; + my $tail = pop(@t); + my $t = expand(join("", (map { /\s+\z/ ? ( $_ ) : ($_, ' ') } @t), $tail)); + my $lead = $ip; + my $ll = $columns - length(expand($ip)) - 1; + $ll = 0 if $ll < 0; + my $nll = $columns - length(expand($xp)) - 1; + my $nl = ""; + my $remainder = ""; + + use re 'taint'; + + pos($t) = 0; + while ($t !~ /\G\s*\Z/gc) { + if ($t =~ /\G([^\n]{0,$ll})($break|\z)/xmgc) { + $r .= $unexpand + ? unexpand($nl . $lead . $1) + : $nl . $lead . $1; + $remainder = $2; + } elsif ($huge eq 'wrap' && $t =~ /\G([^\n]{$ll})/gc) { + $r .= $unexpand + ? unexpand($nl . $lead . $1) + : $nl . $lead . $1; + $remainder = $separator; + } elsif ($huge eq 'overflow' && $t =~ /\G([^\n]*?)($break|\z)/xmgc) { + $r .= $unexpand + ? unexpand($nl . $lead . $1) + : $nl . $lead . $1; + $remainder = $2; + } elsif ($huge eq 'die') { + die "couldn't wrap '$t'"; + } else { + die "This shouldn't happen"; + } + + $lead = $xp; + $ll = $nll; + $nl = $separator; + } + $r .= $remainder; + + print "-----------$r---------\n" if $debug; + + print "Finish up with '$lead'\n" if $debug; + + $r .= $lead . substr($t, pos($t), length($t)-pos($t)) + if pos($t) ne length($t); + + print "-----------$r---------\n" if $debug;; + + return $r; +} + +sub fill +{ + my ($ip, $xp, @raw) = @_; + my @para; + my $pp; + + for $pp (split(/\n\s+/, join("\n",@raw))) { + $pp =~ s/\s+/ /g; + my $x = wrap($ip, $xp, $pp); + push(@para, $x); + } + + # if paragraph_indent is the same as line_indent, + # separate paragraphs with blank lines + + my $ps = ($ip eq $xp) ? "\n\n" : "\n"; + return join ($ps, @para); +} + +1; +__END__ + +=head1 NAME + +Text::Wrap - line wrapping to form simple paragraphs + +=head1 SYNOPSIS + +B + + use Text::Wrap + + $initial_tab = "\t"; # Tab before first line + $subsequent_tab = ""; # All other lines flush left + + print wrap($initial_tab, $subsequent_tab, @text); + print fill($initial_tab, $subsequent_tab, @text); + + @lines = wrap($initial_tab, $subsequent_tab, @text); + + @paragraphs = fill($initial_tab, $subsequent_tab, @text); + +B + + use Text::Wrap qw(wrap $columns $huge); + + $columns = 132; # Wrap at 132 characters + $huge = 'die'; + $huge = 'wrap'; + $huge = 'overflow'; + +B + + use Text::Wrap + + $Text::Wrap::columns = 72; + print wrap('', '', @text); + +=head1 DESCRIPTION + +C is a very simple paragraph formatter. It formats a +single paragraph at a time by breaking lines at word boundries. +Indentation is controlled for the first line (C<$initial_tab>) and +all subsequent lines (C<$subsequent_tab>) independently. Please note: +C<$initial_tab> and C<$subsequent_tab> are the literal strings that will +be used: it is unlikley you would want to pass in a number. + +Text::Wrap::fill() is a simple multi-paragraph formatter. It formats +each paragraph separately and then joins them together when it's done. It +will destory any whitespace in the original text. It breaks text into +paragraphs by looking for whitespace after a newline. In other respects +it acts like wrap(). + +=head1 OVERRIDES + +C has a number of variables that control its behavior. +Because other modules might be using C it is suggested +that you leave these variables alone! If you can't do that, then +use C when you change the +values so that the original value is restored. This C trick +will not work if you import the variable into your own namespace. + +Lines are wrapped at C<$Text::Wrap::columns> columns. C<$Text::Wrap::columns> +should be set to the full width of your output device. In fact, +every resulting line will have length of no more than C<$columns - 1>. + +It is possible to control which characters terminate words by +modifying C<$Text::Wrap::break>. Set this to a string such as +C<'[\s:]'> (to break before spaces or colons) or a pre-compiled regexp +such as C (to break before spaces or apostrophes). The +default is simply C<'\s'>; that is, words are terminated by spaces. +(This means, among other things, that trailing punctuation such as +full stops or commas stay with the word they are "attached" to.) + +Beginner note: In example 2, above C<$columns> is imported into +the local namespace, and set locally. In example 3, +C<$Text::Wrap::columns> is set in its own namespace without importing it. + +C starts its work by expanding all the tabs in its +input into spaces. The last thing it does it to turn spaces back +into tabs. If you do not want tabs in your results, set +C<$Text::Wrap::unexapand> to a false value. Likewise if you do not +want to use 8-character tabstops, set C<$Text::Wrap::tabstop> to +the number of characters you do want for your tabstops. + +If you want to separate your lines with something other than C<\n> +then set C<$Text::Wrap::seporator> to your preference. + +When words that are longer than C<$columns> are encountered, they +are broken up. C adds a C<"\n"> at column C<$columns>. +This behavior can be overridden by setting C<$huge> to +'die' or to 'overflow'. When set to 'die', large words will cause +C to be called. When set to 'overflow', large words will be +left intact. + +Historical notes: 'die' used to be the default value of +C<$huge>. Now, 'wrap' is the default value. + +=head1 EXAMPLE + + print wrap("\t","","This is a bit of text that forms + a normal book-style paragraph"); + +=head1 AUTHOR + +David Muir Sharnoff with help from Tim Pierce and +many many others. + diff --git a/testsuite/tests-to-run/parallel-freebsd.sh b/testsuite/tests-to-run/parallel-freebsd.sh index 85303871..41a011c9 100644 --- a/testsuite/tests-to-run/parallel-freebsd.sh +++ b/testsuite/tests-to-run/parallel-freebsd.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later echo "### These tests requires VirtualBox running with the following images" -SERVER1=freebsd11 +SERVER1=freebsd12 SSHUSER1=vagrant SSHLOGIN1=$SSHUSER1@$SERVER1 echo $SSHUSER1@$SERVER1 diff --git a/testsuite/tests-to-run/parallel-manual.sh b/testsuite/tests-to-run/parallel-manual.sh index 5757777a..5c53e330 100755 --- a/testsuite/tests-to-run/parallel-manual.sh +++ b/testsuite/tests-to-run/parallel-manual.sh @@ -6,6 +6,18 @@ # These fail regularly +par_ll_asian() { + echo '### --ll with Asian wide chars mess up display' + echo 'bug #63878: Wide East Asian chars in --latest-line' + p="parallel --ll --color --tag" + COLUMNS=80 $p echo tag fits, line fits a{}b{}c \ + ::: ヌー平行 + COLUMNS=80 $p echo tag fits, line too long a{}b{}c \ + ::: ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー + COLUMNS=80 $p echo tag too long a{}b{}c \ + ::: ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行ヌー平行a +} + par_ll_tag() { parallel --tag --ll -q printf "a\n{}\n" ::: should-be-tagged-A parallel --tag --ll -q printf "a\n\r{}\n" ::: should-be-tagged-B diff --git a/testsuite/tests-to-run/parallel-remote1.sh b/testsuite/tests-to-run/parallel-remote1.sh index e47851d8..51aa601a 100644 --- a/testsuite/tests-to-run/parallel-remote1.sh +++ b/testsuite/tests-to-run/parallel-remote1.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server4 SERVER3=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant diff --git a/testsuite/tests-to-run/parallel-tutorial.sh b/testsuite/tests-to-run/parallel-tutorial.sh index dedffa19..fa3668cb 100644 --- a/testsuite/tests-to-run/parallel-tutorial.sh +++ b/testsuite/tests-to-run/parallel-tutorial.sh @@ -92,8 +92,10 @@ perl -ne '$/="\n\n"; /^Output/../^[^O]\S/ and next; /^ / and print;' "$testsuit s/^[A-Z][A-Z0-9_]*\s$//; # Fails often due to race s/cat: input_file: No such file or directory\n//; - s{rsync: link_stat ".*/home/parallel/input_file.out" .*\n}{}; + s{rsync: .* link_stat ".*/home/parallel/input_file.out" .*\n}{}; s{rsync error: some files/attrs were not transferred .*\n}{}; + s{Give up after 2 secs\n}{}; + s{parallel: Warning: Semaphore timed out. Exiting.\n}{}; s{.* GtkDialog .*\n}{}; s{tried 1}{}; s/^\s*\n//; diff --git a/testsuite/tests-to-run/test17.sh b/testsuite/tests-to-run/test17.sh index 9b68fed1..b8f762ac 100755 --- a/testsuite/tests-to-run/test17.sh +++ b/testsuite/tests-to-run/test17.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant SSHLOGIN1=$SSHUSER1@$SERVER1 diff --git a/testsuite/tests-to-run/test18.sh b/testsuite/tests-to-run/test18.sh index 01318189..d56228df 100644 --- a/testsuite/tests-to-run/test18.sh +++ b/testsuite/tests-to-run/test18.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant SSHLOGIN1=$SSHUSER1@$SERVER1 diff --git a/testsuite/tests-to-run/test19.sh b/testsuite/tests-to-run/test19.sh index 3559e642..6045cec2 100644 --- a/testsuite/tests-to-run/test19.sh +++ b/testsuite/tests-to-run/test19.sh @@ -7,7 +7,7 @@ # TODO return multiple SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant #SSHLOGIN1=parallel@$SERVER1 diff --git a/testsuite/tests-to-run/test23.sh b/testsuite/tests-to-run/test23.sh index 3f633d31..70d33c5b 100644 --- a/testsuite/tests-to-run/test23.sh +++ b/testsuite/tests-to-run/test23.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant #SSHLOGIN1=parallel@$SERVER1 diff --git a/testsuite/tests-to-run/test37.sh b/testsuite/tests-to-run/test37.sh index cf0bde6e..31a5bfdd 100644 --- a/testsuite/tests-to-run/test37.sh +++ b/testsuite/tests-to-run/test37.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant SSHLOGIN1=$SSHUSER1@$SERVER1 diff --git a/testsuite/tests-to-run/test60.sh b/testsuite/tests-to-run/test60.sh index 724636da..17aafbf1 100644 --- a/testsuite/tests-to-run/test60.sh +++ b/testsuite/tests-to-run/test60.sh @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later SERVER1=parallel-server1 -SERVER2=parallel-server2 +SERVER2=parallel-server3 SSHUSER1=vagrant SSHUSER2=vagrant export SSHLOGIN1=$SSHUSER1@$SERVER1 diff --git a/testsuite/wanted-results/parallel-freebsd b/testsuite/wanted-results/parallel-freebsd index d2e4dbfb..cdf8bfc6 100644 --- a/testsuite/wanted-results/parallel-freebsd +++ b/testsuite/wanted-results/parallel-freebsd @@ -1,5 +1,5 @@ ### These tests requires VirtualBox running with the following images -vagrant@freebsd11 +vagrant@freebsd12 par_compress_pipe par_compress_pipe 2>&1 par_compress_pipe Test --compress --pipe par_compress_pipe 1000 1000 3893 diff --git a/testsuite/wanted-results/parallel-local-0.3s b/testsuite/wanted-results/parallel-local-0.3s index 5b8049b2..2a3c96b5 100644 --- a/testsuite/wanted-results/parallel-local-0.3s +++ b/testsuite/wanted-results/parallel-local-0.3s @@ -336,9 +336,9 @@ par_link_files_as_only_arg bug #50685: single ::::+ does not work par_link_files_as_only_arg 1 1 1 par_link_files_as_only_arg 2 2 2 par_link_files_as_only_arg 3 3 3 -par_ll_long_followed_by_short par_ll_long_followed_by_short A very long line -par_ll_long_followed_by_short  par_ll_long_followed_by_short A very long line -par_ll_long_followed_by_short  par_ll_long_followed_by_short OK +par_ll_long_followed_by_short par_ll_long_followed_by_short A very long line +par_ll_long_followed_by_short  par_ll_long_followed_by_short A very long line +par_ll_long_followed_by_short  par_ll_long_followed_by_short OK par_locale_quoting ### quoting in different locales par_locale_quoting £`/tmp/test£` par_locale_quoting $LC_ALL £`/tmp/test£` diff --git a/testsuite/wanted-results/parallel-local-ssh4 b/testsuite/wanted-results/parallel-local-ssh4 index 7b67a930..a1e04aac 100644 --- a/testsuite/wanted-results/parallel-local-ssh4 +++ b/testsuite/wanted-results/parallel-local-ssh4 @@ -153,22 +153,22 @@ par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 0 par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 4 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 0 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 4 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 0 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 4 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 0 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 4 par_--tmux_different_shells # command is currently too long for csh. Maybe it can be fixed? -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 0 -par_--tmux_different_shells See output with: tmux -S /tmp/tmsXXXXX attach +par_--tmux_different_shells See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_different_shells 4 par_--tmux_length ### works par_--tmux_length See output with: tmux -S /TMP/tmsXXXXX attach @@ -179,6 +179,8 @@ par_--tmux_length See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_length See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_length See output with: tmux -S /TMP/tmsXXXXX attach par_--tmux_length See output with: tmux -S /TMP/tmsXXXXX attach +par__test_different_rsync_versions ### different versions of rsync need fixups +par__test_different_rsync_versions ### no output is good par_bigvar_csh ### csh par_bigvar_csh 3 big vars run remotely - length(base64) > 1000 par_bigvar_csh 1 200 692 diff --git a/testsuite/wanted-results/parallel-local-ssh5 b/testsuite/wanted-results/parallel-local-ssh5 index 40bb7c0b..1c1f037c 100644 --- a/testsuite/wanted-results/parallel-local-ssh5 +++ b/testsuite/wanted-results/parallel-local-ssh5 @@ -67,7 +67,7 @@ par_--ssh_autossh AUTOSSH_DEBUG - turn logging to maximum verbosity an par_--ssh_autossh stderr par_--ssh_autossh par_--ssh_autossh rsync: connection unexpectedly closed (0 bytes received so far) [Receiver] -par_--ssh_autossh rsync error: error in rsync protocol data stream (code 12) at io.c(228) [Receiver=3.2.3] +par_--ssh_autossh rsync error: error in rsync protocol data stream (code 12) at io.c(231) [Receiver=3.2.7] par_--ssh_autossh foo_autossh par_--shellquote_command_len ### test quoting will not cause a crash if too long par_--shellquote_command_len -Slo -j10 " 1 1 1 1 4 diff --git a/testsuite/wanted-results/parallel-local10 b/testsuite/wanted-results/parallel-local10 index 7db53ceb..89ba38f0 100644 --- a/testsuite/wanted-results/parallel-local10 +++ b/testsuite/wanted-results/parallel-local10 @@ -2,3 +2,5 @@ ### See if we get compile error perl ### See if we read modules outside perllib +### Test make .deb package +To install the GNU Parallel Debian package, run: diff --git a/testsuite/wanted-results/parallel-local12 b/testsuite/wanted-results/parallel-local12 index b1497b19..50268da6 100644 --- a/testsuite/wanted-results/parallel-local12 +++ b/testsuite/wanted-results/parallel-local12 @@ -91,8 +91,6 @@ https://lists.gnu.org/archive/html/parallel/2013-11/msg00006.html https://www.gnu.org/software/parallel/parallel_design.html#citation-notice https://git.savannah.gnu.org/cgit/parallel.git/tree/doc/citation-notice-faq.txt -mentioned in the release notes of next version of GNU Parallel. - Type: 'will cite' and press enter. > \ No newline at end of file diff --git a/testsuite/wanted-results/parallel-local150 b/testsuite/wanted-results/parallel-local150 index a8244c39..03ef3f61 100644 --- a/testsuite/wanted-results/parallel-local150 +++ b/testsuite/wanted-results/parallel-local150 @@ -141,9 +141,6 @@ echo "### bug #35268: shell_quote doesn't treats [] brackets correctly" ### bug #35268: shell_quote doesn't treats [] brackets correctly touch /tmp/foo1; stdout parallel echo ::: '/tmp/foo[123]'; rm /tmp/foo1 /tmp/foo[123] -echo '### Test make .deb package'; cd ~/privat/parallel/packager/debian; stdout make | grep 'To install the GNU Parallel Debian package, run:' -### Test make .deb package -To install the GNU Parallel Debian package, run: echo '### Test basic --arg-sep' ### Test basic --arg-sep parallel -k echo ::: a b diff --git a/testsuite/wanted-results/parallel-macos b/testsuite/wanted-results/parallel-macos index 252fa68f..b13fca17 100644 --- a/testsuite/wanted-results/parallel-macos +++ b/testsuite/wanted-results/parallel-macos @@ -1,7 +1,7 @@ -par_big_func 1 3XXX 90XXX -par_big_func 1 3XXX 90XXX -par_big_func 1 3XXX 90XXX -par_big_func 1 1XXX 47XXX +par_big_func 1 3XXX 91XXX +par_big_func 1 3XXX 91XXX +par_big_func 1 3XXX 91XXX +par_big_func 1 1XXX 46XXX par_big_func_name 18XXX par_big_func_name 18XXX par_big_func_name 18XXX @@ -65,8 +65,7 @@ par_many_func 1 196 4XXX par_many_func 1 196 4XXX par_many_func 1 196 4XXX par_many_func 1 196 4XXX -par_many_func 1 196 4XXX -par_many_func 1 4 68 +par_many_func 1 200 4XXX par_many_var 22XXX par_many_var 22XXX par_many_var 22XXX diff --git a/testsuite/wanted-results/parallel-remote1 b/testsuite/wanted-results/parallel-remote1 index a9a84135..55618373 100644 --- a/testsuite/wanted-results/parallel-remote1 +++ b/testsuite/wanted-results/parallel-remote1 @@ -6,8 +6,8 @@ par_filter_hosts_different_errors ### --filter-hosts - OK, non-such-user, connec par_filter_hosts_different_errors aspire par_filter_hosts_no_ssh_nxserver ### test --filter-hosts with server w/o ssh, non-existing server par_filter_hosts_no_ssh_nxserver vagrant@parallel-server1 -par_filter_hosts_no_ssh_nxserver vagrant@parallel-server2 par_filter_hosts_no_ssh_nxserver vagrant@parallel-server3 +par_filter_hosts_no_ssh_nxserver vagrant@parallel-server4 par_special_ssh ### Test use special ssh par_special_ssh TODO test ssh with > 9 simultaneous par_special_ssh 1 @@ -119,7 +119,7 @@ par_timeout_retries parallel: Warning: ssh 8.8.8.8 echo 8.8.8.8 par_timeout_retries parallel: Warning: This job was killed because it timed out: par_timeout_retries parallel: Warning: ssh 8.8.8.8 echo 8.8.8.8 par_timeout_retries vagrant@parallel-server1 -par_timeout_retries vagrant@parallel-server2 +par_timeout_retries vagrant@parallel-server4 par_timeout_retries vagrant@parallel-server3 par_workdir_in_HOME ### test --workdir . in $HOME par_workdir_in_HOME OK diff --git a/testsuite/wanted-results/parallel-tutorial b/testsuite/wanted-results/parallel-tutorial index 2608c0c3..bbd1cda7 100644 --- a/testsuite/wanted-results/parallel-tutorial +++ b/testsuite/wanted-results/parallel-tutorial @@ -149,6 +149,8 @@ num128 num30000 num8 outdir +tmsFPAdA +tmsNGdem tsv-file.tsv foo /TMP @@ -387,6 +389,16 @@ pre-A-post /usr/bin/bash: -c: line 1: syntax error near unexpected token `)' /usr/bin/bash: -c: line 1: `set a="outdir"; if( { test -d "$a" } ) echo "$a is a dir"' /usr/bin/bash: -c: line 1: syntax error near unexpected token `)' +/usr/bin/bash: -c: line 1: `set a="tempfile"; if( { test -d "$a" } ) echo "$a is a dir"' +/usr/bin/bash: -c: line 1: syntax error near unexpected token `)' +/usr/bin/bash: -c: line 1: `set a="tempfile"; if( { test -d "$a" } ) echo "$a is a dir"' +/usr/bin/bash: -c: line 1: syntax error near unexpected token `)' +/usr/bin/bash: -c: line 1: `set a="tempfile"; if( { test -d "$a" } ) echo "$a is a dir"' +/usr/bin/bash: -c: line 1: syntax error near unexpected token `)' +/usr/bin/bash: -c: line 1: `set a="tmsFPAdA"; if( { test -d "$a" } ) echo "$a is a dir"' +/usr/bin/bash: -c: line 1: syntax error near unexpected token `)' +/usr/bin/bash: -c: line 1: `set a="tmsNGdem"; if( { test -d "$a" } ) echo "$a is a dir"' +/usr/bin/bash: -c: line 1: syntax error near unexpected token `)' /usr/bin/bash: -c: line 1: `set a="tsv-file.tsv"; if( { test -d "$a" } ) echo "$a is a dir"' parallel --tag echo foo-{} ::: A B C A foo-A @@ -871,7 +883,7 @@ For details: see man env_parallel export -f my_func3 parallel -vv --workdir ... --nice 17 --env _ --trc {}.out \ -S $SERVER1 my_func3 {} ::: abc-file -ssh -l parallel lo -- exec mkdir -p ./.TMPWORKDIR && rsync --protocol 30 -rlDzR -e'ssh -l parallel' ./abc-file lo:./.TMPWORKDIR;ssh -l parallel lo -- exec perl -X -e GNU_Parallel_worker,eval+pack+q/H10000000/,join+q//,@ARGV BASE64;_EXIT_status=$?; mkdir -p ./. && rsync --protocol 30 -rlDzR -e'ssh -l parallel' --rsync-path='cd ./.TMPWORKDIR/./.; rsync' -- lo:./abc-file.out ./.;ssh -l parallel lo -- exec 'sh -c '"'"'rm -f ./.TMPWORKDIR/abc-file 2>/dev/null;rmdir ./.TMPWORKDIR/ ./.parallel/tmp/ ./.parallel/ 2>/dev/null;rm -rf ./.TMPWORKDIR;'"'";ssh -l parallel lo -- exec 'sh -c '"'"'rm -f ./.TMPWORKDIR/abc-file.out 2>/dev/null;rmdir ./.TMPWORKDIR/ ./.parallel/tmp/ ./.parallel/ 2>/dev/null;rm -rf ./.TMPWORKDIR;'"'";ssh -l parallel lo -- exec rm -rf .TMPWORKDIR;exit $_EXIT_status; +ssh -l parallel lo -- exec mkdir -p ./.TMPWORKDIR && rsync --protocol 30 --old-args -rlDzR -e'ssh -l parallel' ./abc-file lo:./.TMPWORKDIR;ssh -l parallel lo -- exec perl -X -e GNU_Parallel_worker,eval+pack+q/H10000000/,join+q//,@ARGV BASE64;_EXIT_status=$?; mkdir -p ./. && rsync --protocol 30 --old-args -rlDzR -e'ssh -l parallel' --rsync-path='cd ./.TMPWORKDIR/./.; rsync' -- lo:./abc-file.out ./.;ssh -l parallel lo -- exec 'sh -c '"'"'rm -f ./.TMPWORKDIR/abc-file 2>/dev/null;rmdir ./.TMPWORKDIR/ ./.parallel/tmp/ ./.parallel/ 2>/dev/null;rm -rf ./.TMPWORKDIR;'"'";ssh -l parallel lo -- exec 'sh -c '"'"'rm -f ./.TMPWORKDIR/abc-file.out 2>/dev/null;rmdir ./.TMPWORKDIR/ ./.parallel/tmp/ ./.parallel/ 2>/dev/null;rm -rf ./.TMPWORKDIR;'"'";ssh -l parallel lo -- exec rm -rf .TMPWORKDIR;exit $_EXIT_status; parset myvar1,myvar2 echo ::: a b echo $myvar1 echo $myvar2 @@ -1260,7 +1272,6 @@ Slow started Forced running after 1 sec Slow ended parallel: Warning: Semaphore timed out. Stealing the semaphore. -parallel: Warning: Semaphore timed out. Exiting. parallel --help Usage: parallel [options] [command [arguments]] < list_of_arguments @@ -1325,8 +1336,6 @@ More about funding GNU Parallel and the citation notice: https://lists.gnu.org/archive/html/parallel/2013-11/msg00006.html https://www.gnu.org/software/tempfileallel_design.html#citation-notice https:BASE64-notice-faq.txt -If you send a copy of your published article to tange@gnu.org, it will be -mentioned in the release notes of next version of GNU Parallel. parallel --number-of-cpus parallel --number-of-cores 9 diff --git a/testsuite/wanted-results/test37 b/testsuite/wanted-results/test37 index 040ffc80..82259723 100644 --- a/testsuite/wanted-results/test37 +++ b/testsuite/wanted-results/test37 @@ -73,6 +73,6 @@ perl -pe "\$a=1; print \$a" <(echo foo) 1foo ### Test merging of profiles - sort needed because -k only works on the single machine vagrant@parallel-server1 a -vagrant@parallel-server2 a +vagrant@parallel-server3 a ### Test merging of profiles - sort needed because -k only works on the single machine --plain a diff --git a/testsuite/wanted-results/test60 b/testsuite/wanted-results/test60 index 712a75ba..2d0a2278 100644 --- a/testsuite/wanted-results/test60 +++ b/testsuite/wanted-results/test60 @@ -1,6 +1,6 @@ par_nonall ### Test --nonall par_nonall centos8.localdomain -par_nonall freebsd11.localdomain +par_nonall freebsd12.localdomain par_nonall_basefile ### Test --nonall --basefile par_nonall_basefile /tmp/nonall--basefile par_nonall_basefile /tmp/nonall--basefile @@ -10,8 +10,8 @@ par_nonall_sshloginfile_stdin centos8.localdomain par_nonall_u ### Test --nonall -u - should be interleaved x y x y par_nonall_u 1 centos8.localdomain par_nonall_u 1 centos8.localdomain -par_nonall_u 1 freebsd11.localdomain -par_nonall_u 1 freebsd11.localdomain +par_nonall_u 1 freebsd12.localdomain +par_nonall_u 1 freebsd12.localdomain par_onall ### Test --onall par_onall 1 par_onall 2