I have this example file test.txt:
aba
aca
ada
bcd
baa
I would like to find every occurence where a line starting with a and ending with a is followed by another such line. However, my attempts are futile:
perl -0777 -ne 'print "$1\n" while /(a.*a/n)/sg' test.txt
(...) # Matches the whole file as it starts with "a" and ends with "a\n"
perl -0777 -ne 'print "$1\n" while /(a.*?a\n)/sg' test.txt # What is this even
aba
aca
ada
aa
perl -0777 -ne 'print "$1\n" while /(a.*a\n){2}/sg' test.txt
aca
ada
bcd
baa
perl -0777 -ne 'print "$1\n" while /(a.*?a\n){2}/sg' test.txt
aca
These are my results on OSX. Can someone help me out here with the desired pattern?
Don't use the /s, so you can actually match lines. Also, add ^ and $ with /m to match line starts and ends:
perl -0777 -ne 'print "$1\n" while /^(a.*a)\n(a.*a)$/mg'
Or, process line by line remembering the previous line if it matches.
perl -lne '($q) = /^(a.*a)$/; print $p if $p && $q; $p = $q'
Note that the two solutions differ in how they treat more than two a.*a lines occurring sequentially. See ikegami's comment for an alternative solution.
Related
Using one line of Perl code, what is the shortest way possible to print all the lines between two patterns not including the lines with the patterns?
If this is file.txt:
aaa
START
bbb
ccc
ddd
END
eee
fff
I want to print this:
bbb
ccc
ddd
I can get most of the way there using something like this:
perl -ne 'print if (/^START/../^END/);'
That includes the START and END lines, though.
I can get the job done like this:
perl -ne 'if (/^START/../^END/) { print unless (/^(START)|(END)/); };' file.txt
But that seems redundant.
What I'd really like to do is use lookbehind and lookahead assertions like this:
perl -ne 'print if (/^(?<=START)/../(?=END)/);' file.txt
But that doesn't work and I think I've got something just a little bit wrong in my regex.
These are just some of the variations I've tried that produce no output:
perl -ne 'print if (/^(?<=START)/../^.*$(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../^.*(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../.*(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../^.*(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../$(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../^(?=END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../(?=^END)/);' file.txt
perl -ne 'print if (/^(?<=START)/../.*(?=END)/s);' file.txt
Read the whole file, match, and print.
perl -0777 -e 'print <> =~ /START.*?\n(.*?)END.*?/gs;' file.txt
May drop .*? after START|END if alone on line.
Then drop \n for a blank line between segments.
Read file, split line by START|END, print every odd of #F
perl -0777 -F"START|END" -ane 'print #F[ grep { $_ & 1 } (0..$#F) ]' file.txt
Use END { } block for extra processing. Uses }{ for END { }.
perl -ne 'push #r, $_ if (/^START/../^END/); }{ print "#r[1..$#r-1]"' file.txt
Works as it stands only for a single such segment in the file.
It seems kind of arbitrary to place a single-line restriction on this, but here's one way to do it:
$ perl -wne 'last if /^END/; print if $p; $p = 1 if /^START/;' file.txt
perl -e 'print split(/.*START.|END.*/s, join("", <>))' file.txt
perl -ne 'print if /START/../END/' file.txt | perl -ne 'print unless $.==1 or eof'
perl -ne 'print if /START/../END/' file.txt | sed -e '$d' -n -e '1\!p'
I don't see why you are so insistent on using lookarounds, but here are a couple of ways to do it.
perl -ne 'print if /^(?=START)/../^(?=END)/'
This finds the terminators without actually matching them. A zero-length match which satisfies the lookahead is matched.
Your lookbehind wasn't working because it was trying to find beginning of line ^ with START before it on the same line, which can obviously never match. Factor the ^ into the zero-width assertion and it will work:
perl -ne 'print if /(?<=^START)/../(?<=^END)/'
As suggested in comments by #ThisSuitIsBlackNot you can use the sequence number to omit the START and END tokens.
perl -ne '$s = /^START/../^END/; print if ($s>1 && $s !~ /E0/)'
The lookarounds don't contribute anything useful so I did not develop those examples fully. You can adapt this to one of the lookaround examples above if you care more about using lookarounds than about code maintainability and speed of execution.
Can these two regex expressions ever give a different result?
perl -pe 's/.*c//s'
perl -0777 -pe 's/.*c//s'
Where the .*c can be replaced with anything..
In the .*c case the result is the same
$ echo -e 'a\nb\nc\nd' | perl -pe 's/.*c//s'
a
b
d
$ echo -e 'a\nb\nc\nd' | perl -0777 -pe 's/.*c//'
a
b
d
And the question re the difference between the regexes, is where what is echoed can be replaced with anything too.
Are -0777 and /s interchangeable?
And is it pointless to do both -0777 with /s ?
They mean completely different things and are not interchangeable, even though in some cases they can have the same result.
/s makes . match all characters (including linebreaks); without it . usually means [^\n]
-0777 means read the whole file at once; without it the file is read line by line
/s doesn't change how the input is parsed, -0 does.
-0777 is usually only useful if you are matching across several lines (in which case /s can be helpful). If you are matching line by line, then whether you use /s or not doesn't matter.
For example (using your example), if you would like to remove everything up to the last c, including all the lines, you could use:
echo -e 'a\nb\nc\nd' | perl -0777 -pe 's/.*c//s'
Output:
d
Qtax gives a good answer, i'm just going to include some examples to demonstrate they're not the same or even effectively the same.
These two
$ echo -e 'a\nb\nc\nd' | perl -pe 's/./o/s'
o
o
o
o
$ echo -e 'a\nb\nc\nd' | perl -0777 -pe 's/./o/'
o
b
c
d
These two
$ echo -e 'aa\nbb\ncc\ndd' | perl -0777 -pe 's/./o/'
oa
bb
cc
dd
$ echo -e 'aa\nbb\ncc\ndd' | perl -pe 's/./o/s'
oa
ob
oc
od
These two
$ echo -e 'aa\nbb\ncc\ndd' | perl -pe 's/./o/sg'
oooooooooooo
$ echo -e 'aa\nbb\ncc\ndd' | perl -0777 -pe 's/./o/g'
oo
oo
oo
oo
Those all demonstrate that -0777 and /s are not the same.
In shell scripting, grep -Eo {regex} {file} returns the matched part of the regex. For example:
$ echo \
'For support, visit <http://www.example1.com/support>
You can also visit <http://www.example2.com/products> for information.'
| grep -Eo 'http://[a-z0-9_.-]+/'
http://www.example1.com/
http://www.example2.com/
How would I do this with Perl?
Here are two ways:
In Perl, the special variable $& contains the matched part of the regular expression.
perl -ne 'print "$&\n" if m#http://[a-z0-9_.-]+/#' < input
If your regular expression contains capture groups, the patterns matched by those groups are assigned to the variables $1, $2, ...
perl -ne 'print "$1\n" if m#(http://[a-z0-9_.-]+/)#' < input
To get -o functionality I suggest the following:
echo abcdabcd | perl -lne 'while ($_ =~ s/(bc)//){print $1}'
bc
bc
echo abcdabcd | grep -Eo 'bc'
bc
bc
But for you example I suggest perl -pe 's|(http://[\w-\.]+/).*|$1|g':
echo 'http://www.example1.com/support' | perl -pe 's|(http://[\w-\.]+/).*|$1|g'
http://www.example1.com/
How do I replace regex with $var in this command?
echo "$many_lines" | sed -n '/regex/{g;1!p;};h'
$var could look like fs2#auto-17.
The sed command will output the line immediately before a regex, but not the line containing the regex.
If all this can be done easier with a Perl one-liner, then it is fine with me.
It is not beautiful, but this gives me the previous line to $var which is want I wanted.
echo "$many_lines" | grep -B1 "$var" | grep -v "$var"
In Perl regexes, you can interpolate variable contents into regexes like /$foo/. However, the contents will be interpreted as a pattern. If you want to match the literal content of $foo, you have to escape the metacharacters: $safe = quotemeta $foo; /$safe/. This can be shortended to /\Q$foo\E/, which is what you usually want. A trailing \E is optional.
I don't know if the sed regex engine has a similar feature.
A Perl one-liner: perl -ne'$var = "..."; print $p if /\Q$var/; $p=$_'
Use double quotes instead of single quotes to allow variable expansion:
echo $many_lines | sed -n "/$var/"'{g;1!p;};h'
Since you are looking for a line before the regex, with a single one liner it will not be that trivial and beautiful, but here is how I will do it (Using Perl only):
echo "$many_lines" | perl -nle 'print $. if /\Q$var/' | while read line_no; do
export line_no
echo $many_lines | perl -nle 'print if $. - 1 == $ENV{line_no}'
done
or if you want to do in one line
echo "$many_lines" | perl -nle 'BEGIN {my $content = ""; } $content .= $_; END { while ($content =~ m#([^\n]+)\n[^\n]+\Q$var#gosm) { print $1 }}'
Or this one, should definitely match:
echo "$many_lines" | perl -nle 'BEGIN {my #contents; } push #contents, $_; if ($contents[-1] =~ m#\Q$var#o)') { print $contents[-2] if defined $contents[-2]; }
Or you can use here-documents too, if you don't want to escape the double quotes!
In Perl it looks like this:
$heredoc = <<HEREDOC;
here is your text and $var is your parameter
HEREDOC
Its important to end the heredoc with the same string you began, in my example its "HEREDOC" in a new line!
I'm trying to use a perl one-liner to update some code that spans multiple lines and am seeing some strange behavior. Here's a simple text file that shows the problem I'm seeing:
ABCD START
STOP EFGH
I expected the following to work but it doesn't end up replacing anything:
perl -pi -e 's/START\s+STOP/REPLACE/s' input.txt
After doing some experimenting I found that the \s+ in the original regex will match the newline but not any of the whitespace on the 2nd line, and adding a second \s+ doesn't work either. So for now I'm doing the following workaround, which is to add an intermediate regex that only removes the newline:
perl -pi -e 's/START\s+/START/s' input.txt
This creates the following intermediate file:
ABCD START STOP EFGH
Then I can run the original regex (although the /s is no longer needed):
perl -pi -e 's/START\s+STOP/REPLACE/s' input.txt
This creates the final, desired file:
ABCD REPLACE EFGH
It seems like the intermediate step should not be necessary. Am I missing something?
You were close. You need either -00 or -0777:
perl -0777 -pi -e 's/START\s+/START/' input.txt
perl -p processes the file one line at a time. The regex you have is correct, but it is never matched against the multi-line string.
A simple strategy, assuming the file will fit in memory, is to read the whole thing (do this without -p):
$/ = undef;
$file = <>;
$file =~ s/START\s+STOP/REPLACE/sg;
print $file;
Note, I have added the /g modifier to specify global replacement.
As a shortcut for all that extra boilerplate, you can use your existing script with the -0777 option: perl -0777pi -e 's/START\s+STOP/REPLACE/sg'. Adding /g is still needed if you may need to make multiple replacements within the file.
A hiccup that you might run into, although not with this regex: if the regex were START.+STOP, and a file contains multiple START/STOP pairs, greedy matching of .+ will eat everything from the first START to the last STOP. You can use non-greedy matching (match as little as possible) with .+?.
If you want to use the ^ and $ anchors for line boundaries anywhere in the string, then you also need the /m regex modifier.
A relatively simple one-liner (reading the file in memory):
perl -pi -e 'BEGIN{undef $/;} s/START\s+STOP/REPLACE/sg;' input.txt
Another alternative (not so simple), not reading the file in memory:
perl -ni -e '$a.=$_; \
if ( $a =~ s/START\s+STOP/REPLACE/s ) { print $a; $a=""; } \
END{$a && print $a}' input.txt
perl -MFile::Slurp -e '$content = read_file(shift); $content =~ s/START\s+STOP/REPLACE/s; print $content' input.txt
Here's a one-liner that doesn't read the entire file into memory at once:
perl -i -ne 'if (($x = $last . $_) =~ s/START\n\s*STOP/REPLACE/) \
{ print $x; $last = ""; } else { print $last; $last = $_; } \
print $last if eof ARGV' input.txt