Want to extract the first letter of each word - perl

I basically have a variable COUNTRY along with variables SUBJID and TREAT and I want to concatenate it like this ABC002-123 /NZ/ABC.
Suppose if the COUNTRY variable had the value 'New Zealand'. I want to extract the first letter of each word, But I want extract only the first two letters of the value when there is only one word in the COUNTRY variable. I wanted a to know how to simply the below code. If possible in perl programming.
If COUNTW(COUNTRY) GT 1 THEN
CAT_VAR=
UPCASE(SUBJID||"/"||CAT(SUBSTR(SCAN(COUNTRY,1,' '),1,1),
SUBSTR(SCAN(COUNTRY,2,' '),1,1))||"/"||TREAT);

my #COUNTRY = ("New Zealand", "Germany");
# 'NZ', 'GE'
my #two_letters = map {
my #r = /\s/ ? /\b(\w)/g : /(..)/;
uc(join "", #r);
} #COUNTRY;

The SAS Perl Regular Expression solution is to use CALL PRXNEXT along with PRXPOXN or CALL PRXPOSN (or a similar function, if you prefer):
data have;
infile datalines truncover;
input #1 country $20.;
datalines;
New Zealand
Australia
Papua New Guinea
;;;;
run;
data want;
set have;
length country_letter $5.;
prx_1 = prxparse('~(?:\b([a-z])[a-z]*\b)+~io');
length=0;
start=1;
stop = length(country);
position=0;
call prxnext(prx_1,start,stop,country,position,length);
do while (position gt 0);
matchletter = prxposn(prx_1,1,country);
country_letter = cats(country_letter,matchletter);
call prxnext(prx_1,start,stop,country,position,length);
put i= position= start= stop=;
end;
run;

I realize the OP might not be interested in another answer, but for other users browsing this thread and not wanting to use Perl expressions I suggest the following simple solution (for the original COUNTRY variable):
FIRST_LETTERS = compress(propcase(COUNTRY),'','l');
The propcase functions capitalizes the first letters of each word and puts the other ones in lower case. The compress function with 'l' modifier deletes all lower case letters.
COUNTRY may have any number of words.

How about this:
#!/usr/bin/perl
use warnings;
use strict;
my #country = ('New Zealand', 'Germany', 'Tanzania', 'Mozambique', 'Irish Repuublic');
my ($one_word_letters, $two_word_letters, #initials);
foreach (#country){
if ($_ =~ /\s+/){ # Captures CAPs if 'country' contains a space
my ($first_letter, $second_letter) = ($_ =~ /([A-Z])/g);
my ($two_word_letters) = ($first_letter.$second_letter);
push #initials, $two_word_letters; # Add to array for later
}
else { ($one_word_letters) = ($_ =~ /([A-Z][a-z])/); # If 'country' is only one word long, then capture first two letters (CAP+noncap)
push #initials, $one_word_letters; # Add this to the same array
}
}
foreach (#initials){ # Print contents of the capture array:
print "$_\n";
}
Outputs:
NZ
Ge
Ta
Mo
IR
This should do the job provided there really are no 3 word countries. Easily fixed if there are though...

This should do.
#!/usr/bin/perl
$init = &getInitials($ARGV[0]);
if($init)
{
print $init . "\n";
exit 0;
}
else
{
print "invalid name\n";
exit 1;
}
1;
sub getInitials {
$name = shift;
$name =~ m/(^(\S)\S*?\s+(\S)\S*?$)|(^(\S\S)\S*?$)/ig;
if( defined($1) and $1 ne '' ) {
return uc($2.$3);
} elsif( defined($4) and $4 ne '' ) {
return uc($5);
} else {
return 0;
}
}

Related

Extracting info from file rows into columns using whatever it works (PERL, SED, AWK)

Maybe I´m too old for perl/awk/sed, too young to stop programming.
Here is the problem I need to solve:
I have info like this in a TXT file:
Name:
Name 1
Phone:
1111111
Email:
some#email1
DoentMatterInfo1:
whatever1
=
Name:
Name 2
Phone:
22222222
DoentMatterInfo2:
whatever2
Email:
some#email2
=
Name:
Name 3
DoentMatterInfo3:
whatever2
Email:
some#email3
=
Please note that the desired info is in the next line, there is a record separator (=) and very important, some records doesn't have all the info, but could have info that we dont want.
So, the challenge is to extract the desired info, if exist, in an output like:
Name 1 ; 111111 ; some#email1
Name 2 ; 222222 ; some#email2
Name 3 ; ; some#email3
What I have tried that worked a little bit but stills is not what I´m looking for.
1. Using PERL
Using Perl I got the fields that matter:
while (<>) {
if ($_ =~ /Name/) {
print "=\n". scalar <>;
}
if ($_ =~ /Email/) {
print "; ". scalar <>;
}
if ($_ =~ /Phone/) {
print "; ". scalar <>;
}
}
The I got a file like:
Name 1
; 1111111
; some#email1
=
Name 2
; 22222222
; some#email2
=
Name:
Name 3
; some#email3
=
Now with sed I put each record in a single line:
SED
With SED, this command replaces the Line Feed, got the info in a single line:
sed ':a;N;$!ba;s/\n//g' input.txt > out1.txt
And out back the line feed:
sed 's/|=|/\n/g' out1.txt > out2.txt
So I got a file with the info in each line:
Name 1 ; 1111111 ; some#email1
Name 2 ; 22222222 ; some#email2
Name 3 ; some#email3
Still not what I would like to get from coding. I want something better, like being able to fill the missing phone with space, so the second column could be always the phone column. Do you get it?
AS you can see, the poitn is to find a solution, no matter if is using Perl, AWk or SED. I´m trying perl hashes...
Thanks in advance!!
Here is a Perl solution, asked for and attempted
use warnings;
use strict;
use feature 'say';
my #fields = qw(Name Phone Email); # fields to process
my $re_fields = join '|', map { quotemeta } #fields;
my %record;
while (<>) {
if (/^\s*($re_fields):/) {
chomp($record{$1} = <>);
}
elsif (/^\s*=/) {
say join ';', map { $record{$_} // '' } #fields;
%record = ();
}
}
The input is prepared in the array #fields; this is the only place where those names are spelled out, so if more fields need be added to processing just add them here. A regex pattern for matching any one of these fields is also prepared, in $re_fields.
Then we read line by line all files submitted on the command line, using the <> operator.
The if condition captures an expected keyword if there. In the body we read the next line for its value and store it with the key being the captured keyword (need not know which one).
On a line starting with = the record is printed (correctly with the given sample file). I put nothing for missing fields (no spaces) and no extra spaces around ;. Adjust the output format as desired.
In order to collect records throughout and process further (or just print) later, add them to a suitable data structure instead of printing. What storage to choose depends on what kind of processing is envisioned. The simplest way to go is to add strings for each output record to an array
my (#records, %record);
while (<>) {
...
elsif (/^\s*=/) {
push #records, join ';', map { $record{$_} // '' } #fields;
%record = ();
}
}
Now #records has ready strings for all records, which can be printed simply as
say for #records;
But if more involved processing may be needed then better store in an array copies of %record as hash references, so that individual components can later be manipulated more easily
my (#records, %record);
while (<>) {
...
elsif (/^\s*=/) {
# Add a key to the hash for any fields that are missing
$record{$_} //= '' for #fields;
push #records, { %record };
%record = ();
}
}
I add a key for possibly missing fields, so that the hashrefs have all expected keys, and I assign an empty string to it. Another option is to assign undef.
Now you can access individual fields in each record as
foreach my $rec (#records) {
foreach my $fld (sort keys %$rec) {
say "$fld -> $rec->{$fld}"
}
}
or of course just print the whole thing using Data::Dumper or such.
This will work using any awk in any shell on every UNIX box:
$ cat tst.awk
BEGIN { OFS=" ; " }
$0 == "=" {
print f["Name:"], f["Phone:"], f["Email:"]
delete f
lineNr = 0
next
}
++lineNr % 2 { tag = $0; next }
{ f[tag] = $0 }
.
$ awk -f tst.awk file
Name 1 ; 1111111 ; some#email1
Name 2 ; 22222222 ; some#email2
Name 3 ; ; some#email3
I would do it like this:
$ cat prog.awk
#!/bin/awk -f
BEGIN { OFS = ";" }
/^(Name|Phone|Email):$/ { getline arr[$0] ; next }
/^=$/ { print arr["Name:"], arr["Phone:"], arr["Email:"] ; delete arr }
Explanation:
In the BEGIN block, define the output field separator (semicolon).
For each line in the input file, if the line (in its entirety) equals Name: or Phone: or Email: then assign that string to the key and the value of the following line to the value of an element of the associative array arr. (That is how getline can be used to assign a value to a variable.) Then skip the next rule.
If the line is =, print the three values from the arr associative array, and then clear out the array (reset all the values to the empty string).
* * * *
Make it executable:
chmod +x prog.awk
Use it:
$ ./prog.awk file.txt
Name 1;1111111;some#email1
Name 2;22222222;some#email2
Name 3;;some#email3
Note - a missing value is indicated by two consecutive semicolons (not by a space). Using space as placeholder for NULL is a common bad practice (especially in relational databases, but in flat files too). You can change this to use NULL as placeholder, I am not terribly interested in that bit of the problem.
Input file format is easy to parse: split on =\n into records, split each record on \n into a hash and push the hash into #result array.
Then just output each element of #result array with specifying fields of interest.
use strict;
use warnings;
use feature 'say';
use Data::Dumper;
my #result;
my $data = do { local $/; <DATA> };
my #records = split('=\n?',$data);
push #result, {split "\n", $_} for #records;
say Dumper(\#result);
my #fields = qw/Name: Phone: Email:/;
for my $record (#result) {
$record->{$_} = $record->{$_} || '' for #fields;
say join('; ', #$record{#fields});
}
__DATA__
Name:
Name 1
Phone:
1111111
Email:
some#email1
DoentMatterInfo1:
whatever1
=
Name:
Name 2
Phone:
22222222
DoentMatterInfo2:
whatever2
Email:
some#email2
=
Name:
Name 3
DoentMatterInfo3:
whatever2
Email:
some#email3
=
Output
$VAR1 = [
{
'DoentMatterInfo1:' => 'whatever1',
'Name:' => 'Name 1',
'Email:' => 'some#email1',
'Phone:' => '1111111'
},
{
'Phone:' => '22222222',
'Email:' => 'some#email2',
'Name:' => 'Name 2',
'DoentMatterInfo2:' => 'whatever2'
},
{
'DoentMatterInfo3:' => 'whatever2',
'Name:' => 'Name 3',
'Email:' => 'some#email3'
}
];
Name 1; 1111111; some#email1
Name 2; 22222222; some#email2
Name 3; ; some#email3

Regular expression to print a string from a command outpout

I have written a function that uses regex and prints the required string from a command output.
The script works as expected. But it's does not support a dynamic output. currently, I use regex for "icmp" and "ok" and print the values. Now, type , destination and return code could change. There is a high chance that command doesn't return an output at all. How do I handle such scenarios ?
sub check_summary{
my ($self) = #_;
my $type = 0;
my $return_type = 0;
my $ipsla = $self->{'ssh_obj'}->exec('show ip sla');
foreach my $line( $ipsla) {
if ( $line =~ m/(icmp)/ ) {
$type = $1;
}
if ( $line =~ m/(OK)/ ) {
$return_type = $1;
}
}
INFO ($type,$return_type);
}
command Ouptut :
PSLAs Latest Operation Summary
Codes: * active, ^ inactive, ~ pending
ID Type Destination Stats Return Last
(ms) Code Run
-----------------------------------------------------------------------
*1 icmp 192.168.25.14 RTT=1 OK 1 second ago
Updated to some clarifications -- we need only the last line
As if often the case, you don't need a regex to parse the output as shown. You have space-separated fields and can just split the line and pick the elements you need.
We are told that the line of interest is the last line of the command output. Then we don't need the loop but can take the last element of the array with lines. It is still unclear how $ipsla contains the output -- as a multi-line string or perhaps as an arrayref. Since it is output of a command I'll treat it as a multi-line string, akin to what qx returns. Then, instead of the foreach loop
my #lines = split '\n', $ipsla; # if $ipsla is a multi-line string
# my #lines = #$ipsla; # if $ipsla is an arrayref
pop #lines while $line[-1] !~ /\S/; # remove possible empty lines at end
my ($type, $return_type) = (split ' ', $lines[-1])[1,4];
Here are some comments on the code. Let me know if more is needed.
We can see in the shown output that the fields up to what we need have no spaces. So we can split the last line on white space, by split ' ', $lines[-1], and take the 2nd and 5th element (indices 1 and 4), by ( ... )[1,4]. These are our two needed values and we assign them.
Just in case the output ends with empty lines we first remove them, by doing pop #lines as long as the last line has no non-space characters, while $lines[-1] !~ /\S/. That is the same as
while ( $lines[-1] !~ /\S/ ) { pop #lines }
Original version, edited for clarifications. It is also a valid way to do what is needed.
I assume that data starts after the line with only dashes. Set a flag once that line is reached, process the line(s) if the flag is set. Given the rest of your code, the loop
my $data_start;
foreach (#lines)
{
if (not $data_start) {
$data_start = 1 if /^\s* -+ \s*$/x; # only dashes and optional spaces
}
else {
my ($type, $return_type) = (split)[1,4];
print "type: $type, return code: $return_type\n";
}
}
This is a sketch until clarifications come. It also assumes that there are more lines than one.
I'm not sure of all possibilities of output from that command so my regular expression may need tweaking.
I assume the goal is to get the values of all columns in variables. I opted to store values in a hash using the column names as the hash keys. I printed the results for debugging / demonstration purposes.
use strict;
use warnings;
sub check_summary {
my ($self) = #_;
my %results = map { ($_,undef) } qw(Code ID Type Destination Stats Return_Code Last_Run); # Put results in hash, use column names for keys, set values to undef.
my $ipsla = $self->{ssh_obj}->exec('show ip sla');
foreach my $line (#$ipsla) {
chomp $line; # Remove newlines from last field
if($line =~ /^([*^~])([0-9]+)\s+([a-z]+)\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+([[:alnum:]=]+)\s+([A-Z]+)\s+([^\s].*)$/) {
$results{Code} = $1; # Code prefixing ID
$results{ID} = $2;
$results{Type} = $3;
$results{Destination} = $4;
$results{Stats} = $5;
$results{Return_Code} = $6;
$results{Last_Run} = $7;
}
}
# Testing
use Data::Dumper;
print Dumper(\%results);
}
# Demonstrate
check_summary();
# Commented for testing
#INFO ($type,$return_type);
Worked on the submitted test line.
EDIT:
Regular expressions allow you to specify patterns instead of the exact text you are attempting to match. This is powerful but complicated at times. You need to read the Perl Regular Expression documentation to really learn them.
Perl regular expressions also allow you to capture the matched text. This can be done multiple times in a single pattern which is how we were able to capture all the columns with one expression. The matches go into numbered variables...
$1
$2

Instr Equivalent in perl?

A variable named RestrictedNames holds the list of restricted user names. SplitNames is an array variable which holds the complete set of user name. Now I have to check whether current name is found in RestrictedNames variable like using instr.
#SplitNames = ("naag algates","arvind singh","abhay avasti","luv singh","new algates") and now i want to block all the surnames which has "singh" ,"algates" etc.
#SplitNames = ("naag algates","arvind singh","abhay avasti","luv singh","new algates")
$RestrictedNames="tiwary singh algates n2 n3 n4 n5 n6";
for(my $i=0;$i<#SplitNames;$i++)
{
if($RestrictedNames =~ m/^$SplitNames[$i]/ ) //google'd this condition, still fails
{
print "$SplitNames[$i] is a restricted person";
}
}
You should modify this line:
if($RestrictedNames =~ m/^$SplitNames[$i]/ )
to
if($RestrictedNames =~ m/$SplitNames[$i]/ )
^ looks for a match from the beginning.
For more details about perl metacharacters, see here
EDIT:
If you need blocking based on surnames, try this code in the for-loop body.
my #tokens = split(' ', $SplitNames[$i]); # splits name on basis of spaces
my $surname = $tokens[$#tokens]; # takes the last token
if($RestrictedNames =~ m/$surname/ )
{
print "$SplitNames[$i] is a restricted person\n";
}
Don't try dealing with a string of restricted names, deal with an array.
Then just use the smart match operator (~~ or two tilde characters) to see if a given string is in it.
#!/usr/bin/perl
use v5.12;
use strict;
use warnings;
my $RestrictedNames="n1 n2 n3 n4 n5 n6 n7 n8 n9";
my #restricted_names = split " ", $RestrictedNames;
say "You can't have foo" if 'foo' ~~ #restricted_names;
say "You can't have bar" if 'bar' ~~ #restricted_names;
say "You can't have n1" if 'n1' ~~ #restricted_names;
say "You can't have n1a" if 'n1a' ~~ #restricted_names;
Try something like below using Hash Slice:
my #users = ( "n10", "n12", "n13", "n4", "n5" );
my #r_users = ( "n1", "n2", "n3", "n4", "n5", "n6", "n7", "n8", "n9" ) ;
my %check;
#check{#r_users} = ();
foreach my $user ( #users ) {
if ( exists $check{$user} ) {
print"Restricted User: $user \n";
}
}
Most idiomatic way would be to create a hash of the restricted names, then split the surname from the name and check if the surname is in the hash.
use strict;
use warnings;
my #SplitNames = ("naag algates","arvind singh","abhay avasti","luv singh","new algates");
my $RestrictedNames = "tiwar y singh algates n2 n3 n4 n5 n6";
# Create hash of restricted names
my %restricted;
map { $restricted{$_}++ } split(' ', $RestrictedNames);
# Loop over names and check if surname is in the hash
for my $name (#SplitNames) {
my $surname = (split(' ', $name))[-1];
if ( $restricted{$surname} ) {
print "$name is a restricted person\n";
}
}
Please note that the split function normally takes a RegEx. However using ' ' with split is a special case. It splits on any length of whitespace, and also ignores any leading whitespace, so it's useful for splitting strings of individual words.
FYI, the equivalent to instr in perl is to use index($string, $substring). If $substring does not occur inside $string it will return -1. Any other value means $string contains $substring. However, when comparing lists it's much less hassle to use a hash like I have shown above... and unlike index, it won't match 'joyce' when you really only meant to match 'joy'.

How can i detect symbols using regular expression in perl?

Please how can i use regular expression to check if word starts or ends with a symbol character, also how to can i process the text within the symbol.
Example:
(text) or te-xt, or tex't. or text?
change it to
(<t>text</t>) or <t>te-xt</t>, or <t>tex't</t>. or <t>text</t>?
help me out?
Thanks
I assume that "word" means alphanumeric characters from your example? If you have a list of permitted characters which constitute a valid word, then this is enough:
my $string = "x1 .text1; 'text2 \"text3;\"";
$string =~ s/([a-zA-Z0-9]+)/<t>$1<\/t>/g;
# Add more to character class [a-zA-Z0-9] if needed
print "$string\n";
# OUTPUT: <t>x1</t> .<t>text1</t>; '<t>text2</t> "<t>text3</t>;"
UPDATE
Based on your example you seem to want to DELETE dashes and apostrophes, if you want to delete them globally (e.g. whether they are inside the word or not), before the first regex, you do
$string =~ s/['-]//g;
I am using DVK's approach here, but with a slight modification. The difference is that her/his code would also put the tags around all words that don't contain/are next to a symbol, which (according to the example given in the question) is not desired.
#!/usr/bin/perl
use strict;
use warnings;
sub modify {
my $input = shift;
my $text_char = 'a-zA-Z0-9\-\''; # characters that are considered text
# if there is no symbol, don't change anything
if ($input =~ /^[a-zA-Z0-9]+$/) {
return $input;
}
else {
$input =~ s/([$text_char]+)/<t>$1<\/t>/g;
return $input;
}
}
my $initial_string = "(text) or te-xt, or tex't. or text?";
my $expected_string = "(<t>text</t>) or <t>te-xt</t>, or <t>tex't</t>. or <t>text</t>?";
# version BEFORE edit 1:
#my #aux;
# take the initial string apart and process it one word at a time
#my #string_list = split/\s+/, $initial_string;
#
#foreach my $string (#string_list) {
# $string = modify($string);
# push #aux, $string;
#}
#
# put the string together again
#my $final_string = join(' ', #aux);
# ************ EDIT 1 version ************
my $final_string = join ' ', map { modify($_) } split/\s+/, $initial_string;
if ($final_string eq $expected_string) {
print "it worked\n";
}
This strikes me as a somewhat long-winded way of doing it, but it seemed quicker than drawing up a more sophisticated regex...
EDIT 1: I have incorporated the changes suggested by DVK (using map instead of foreach). Now the syntax highlighting is looking even worse than before; I hope it doesn't obscure anything...
This takes standard input and processes it to and prints on Standard output.
while (<>) {
s {
( [a-zA-z]+ ) # word
(?= [,.)?] ) # a symbol
}
{<t>$1</t>}gx ;
print ;
}
You might need to change the bit to match the concept of word.
I have use the x modifeid to allow the regexx to be spaced over more than one line.
If the input is in a Perl variable, try
$string =~ s{
( [a-zA-z]+ ) # word
(?= [,.)?] ) # a symbol
}
{<t>$1</t>}gx ;

Parsing files that use synonyms

If I had a text file with the following:
Today (is|will be) a (great|good|nice) day.
Is there a simple way I can generate a random output like:
Today is a great day.
Today will be a nice day.
Using Perl or UNIX utils?
Closures are fun:
#!/usr/bin/perl
use strict;
use warnings;
my #gens = map { make_generator($_, qr~\|~) } (
'Today (is|will be) a (great|good|nice) day.',
'The returns this (month|quarter|year) will be (1%|5%|10%).',
'Must escape %% signs here, but not here (%|#).'
);
for ( 1 .. 5 ) {
print $_->(), "\n" for #gens;
}
sub make_generator {
my ($tmpl, $sep) = #_;
my #lists;
while ( $tmpl =~ s{\( ( [^)]+ ) \)}{%s}x ) {
push #lists, [ split $sep, $1 ];
}
return sub {
sprintf $tmpl, map { $_->[rand #$_] } #lists
};
}
Output:
C:\Temp> h
Today will be a great day.
The returns this month will be 1%.
Must escape % signs here, but not here #.
Today will be a great day.
The returns this year will be 5%.
Must escape % signs here, but not here #.
Today will be a good day.
The returns this quarter will be 10%.
Must escape % signs here, but not here %.
Today is a good day.
The returns this month will be 1%.
Must escape % signs here, but not here %.
Today is a great day.
The returns this quarter will be 5%.
Must escape % signs here, but not here #.
Code:
#!/usr/bin/perl
use strict;
use warnings;
my $template = 'Today (is|will be) a (great|good|nice) day.';
for (1..10) {
print pick_one($template), "\n";
}
exit;
sub pick_one {
my ($template) = #_;
$template =~ s{\(([^)]+)\)}{get_random_part($1)}ge;
return $template;
}
sub get_random_part {
my $string = shift;
my #parts = split /\|/, $string;
return $parts[rand #parts];
}
Logic:
Define template of output (my $template = ...)
Enter loop to print random output many times (for ...)
Call pick_one to do the work
Find all "(...)" substrings, and replace them with random part ($template =~ s...)
Print generated string
Getting random part is simple:
receive extracted substring (my $string = shift)
split it using | character (my #parts = ...)
return random part (return $parts[...)
That's basically all. Instead of using function you could put the same logic in s{}{}, but it would be a bit less readable:
$template =~ s{\( ( [^)]+ ) \)}
{ my #parts = split /\|/, $1;
$parts[rand #parts];
}gex;
Sounds like you may be looking for Regexp::Genex. From the module's synopsis:
#!/usr/bin/perl -l
use Regexp::Genex qw(:all);
$regex = shift || "a(b|c)d{2,4}?";
print "Trying: $regex";
print for strings($regex);
# abdd
# abddd
# abdddd
# acdd
# acddd
# acdddd
Use a regex to match each parenthetical (and the text inside it).
Use a string split operation (pipe delimiter) on the text inside of the matched parenthetical to get each of the options.
Pick one randomly.
Return it as the replacement for that capture.
Smells like a recursive algorithm
Edit: misread and thought you wanted all possibilities
#!/usr/bin/python
import re, random
def expand(line, all):
result = re.search('\([^\)]+\)', line)
if result:
variants = result.group(0)[1:-1].split("|")
for v in variants:
expand(line[:result.start()] + v + line[result.end():], all)
else:
all.append(line)
return all
line = "Today (is|will be) a (great|good|nice) day."
all = expand(line, [])
# choose a random possibility at the end:
print random.choice(all)
A similar construct that produces a single random line:
def expand_rnd(line):
result = re.search('\([^\)]+\)', line)
if result:
variants = result.group(0)[1:-1].split("|")
choice = random.choice(variants)
return expand_rnd(
line[:result.start()] + choice + line[result.end():])
else:
return line
Will fail however on nested constructs