PHP preg_split with two delimiters unless a delimiter is within quotes - php

Further on from my previous question about preg_split which was answers super fast, thanks to nick; I would really like to extend the scenario to no split the string when a delimiter is within quotes. For example:
If I have the string foo = bar AND bar=foo OR foobar="foo bar", I'd wish to split the sting on every space or = character but include the = character in the returned array (which works great currently), but I don't want to split the string either of the delimiters are within quotes.
I've got this so far:
<!doctype html>
<?php
$string = 'foo = bar AND bar=foo';
$array = preg_split('/ +|(=)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
?>
<pre>
<?php
print_r($array);
?>
</pre>
Which gets me:
Array
(
[0] => foo
[1] => =
[2] => bar
[3] => AND
[4] => bar
[5] => =
[6] => foo
)
But if I changed the string to:
$string = 'foo = bar AND bar=foo OR foobar = "foo bar"';
I'd really like the array to be:
Array
(
[0] => foo
[1] => =
[2] => bar
[3] => AND
[4] => bar
[5] => =
[6] => foo
[6] => OR
[6] => foobar
[6] => =
[6] => "foo bar"
)
Notice the "foo bar" wasn't split on the space because it's in quotes?
Really not sure how to do this within the RegEx or if there is even a better way but all your help would be very much appreciated!
Thank you all in advance!

Try
$array = preg_split('/(?: +|(=))(?=(?:[^"]*"[^"]*")*[^"]*$)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
The
(?=(?:[^"]*"[^"]*")*[^"]*$)
part is a lookahead assertion making sure that there is an even number of quote characters ahead in the string, therefore it will fail if the current position is between quotes:
(?= # Assert that the following can be matched:
(?: # A group containing...
[^"]*" # any number of non-quote characters followed by one quote
[^"]*" # the same (to ensure an even number of quotes)
)* # ...repeated zero or more times,
[^"]* # followed by any number of non-quotes
$ # until the end of the string
)

I was able to do this by adding quoted strings as a delimiter a-la
"(.*?)"| +|(=)
The quoted part will be captured. It seems like this is a bit tenuous and I did not test it extensively, but it at least works on your example.

But why bother splitting?
After a look at this old question, this simple solution comes to mind, using a preg_match_all rather than a preg_split. We can use this simple regex to specify what we want:
"[^"]*"|\b\w+\b|=
See online demo.

Related

What's the most efficient way to split a sentence into words while maintaining whitespace?

Let's say I have a "sentence" consisting of "words" and spaces such as:
' foo bar foobar '
Specifically a word is any set of characters not including a space. There is one or more spaces between each word. The leading and trailing spaces are optional.
I'd like the simplest way to separate the words and spaces into a collection such as:
[ ' ', 'foo', ' ', 'bar, ' ', 'foobar', ' ']
I tried using explode() with the space character as the separator, but of course, the results did not include the spaces. I tried preg_match() without success, but that may be because I couldn't find the right regular expression.
I could process the sentence character by character, but I feel like there should be an easier way. Any suggestions?
I tried using the following which worked for me:
$s = ' foo bar foobar ';
preg_match_all('/\s+|\w+/', $s, $matches);
print_r($matches);
giving out spaces and words in an array of array:
Array
(
[0] => Array
(
[0] =>
[1] => foo
[2] =>
[3] => bar
[4] =>
[5] => foobar
[6] =>
)
)
But is this the most efficient way? I will leave that to you to judge :)

Regex to Match Passed Function/Method Parameters

I've had a good look around for a question that asked this before; alas, my search for a PHP preg_match search returned no results (maybe my searching skills fell short, I suppose justified considering it's a Regex question!).
Consider the text below:
The quick __("brown ") fox jumps __('over the') lazy __("dog")
Now currently I need to 'scan' for the given method __('') above, whereas it could include the spacing and different quotations ('|"). My best attempt after numerous 'iterations':
(__\("(.*?)"\))|(__\('(.*?)'\))
Or at its simplest form:
__\((.*?)\)
To break this down:
Anything that starts with __
Escaped ( and quotation mark " or '. Thus, \(\"
(.*?) Non-greedy match of all characters
Escaped closing " and last bracket.
| between the two expressions match either/or.
However, this only gets partial matches, and spaces are throwing off the search entirely. Apologies if this has been asked before, please link me if so!
Tester Link for the pattern provided above:
PHP Live Regex Test Tool
When the searched method string uses single quotes it will end up in another capture group than if it has double quotes. So in fact, your regular expression works (except for the spaces, see further down), but you'd have to look at a different index in your result array:
$input = 'The quick __("brown ") fox jumps __(\'over the\') lazy __("dog")';
// using your regular expression:
$res = preg_match_all("/(__\(\"(.*?)\"\))|(__\('(.*?)'\))/", $input, $matches);
print_r ($matches);
Note that you need preg_match_all instead of preg_match to get all matches.
Output:
Array
(
[0] => Array
(
[0] => __("brown ")
[1] => __('over the')
[2] => __("dog")
)
[1] => Array
(
[0] => __("brown ")
[1] =>
[2] => __("dog")
)
[2] => Array
(
[0] => brown
[1] =>
[2] => dog
)
[3] => Array
(
[0] =>
[1] => __('over the')
[2] =>
)
[4] => Array
(
[0] =>
[1] => over the
[2] =>
)
)
So, the result array has 5 elements, the first one representing the complete match, and all the others correspond to the 4 capture groups you have in your regular expression. As the capture groups for single quotes are not those of the double quotes, you'll find the matches at different places.
To "solve" this, you could use a back reference in your regular expression, which would look back to see which was the opening quote (single or double) and require the same to be repeated at the end:
$res = preg_match_all("/__\(([\"'])(.*?)\\1\)/", $input, $matches);
Note the back reference \1 (the backslash had to be escaped with another one). This refers back to the first capture group, where we have ["'] (again an escape was necessary) to match both kinds of quotes.
You also wanted to deal with spaces. On your PHP Live Regex you used a test string that had such spaces between the brackets and quotes. To deal with these so they still match the method strings correctly, the regular expression should get two additional \s*:
$res = preg_match_all("/__\(\s*([\"'])(.*?)\\1\s*\)/", $input, $matches);
Now the output is:
Array
(
[0] => Array
(
[0] => __("brown ")
[1] => __('over the')
[2] => __("dog")
)
[1] => Array
(
[0] => "
[1] => '
[2] => "
)
[2] => Array
(
[0] => brown
[1] => over the
[2] => dog
)
)
... and the text captured by the groups is now nicely arranged.
See this code run on eval.in and PHP Live Regex.
When working with stuff like this, don't forget about escaping:
<?php
ob_start();
?>
The quick __("brown ") fox jumps __( 'over the' ) lazy __("dog").
And __("everyone says \"hi\"").
<?php
$content = ob_get_clean();
$re = <<<RE
/__ \(
\s*
" ( (?: \\\\. | [^"])+ ) "
|
' ( (?: \\\\. | [^'])+ ) '
\s*
\)
/x
RE;
preg_match_all($re, $content, $matches, PREG_SET_ORDER);
foreach($matches as $match)
echo end($match), "\n";
How about this:
(__(\('[^']+'\)|\("[^"]+"\)))
Instead of the non greedy ., use any char but the quotes [^'] or [^"]
Enclose double and single quotes with square brackets as a character class:
$str = 'The quick __( "brown ") fox jumps __(\'over the\') lazy __("dog")';
preg_match_all("/__\(\s*([\"']).*?\\1\s*\)/ium", $str, $matches);
echo '<pre>';
var_dump($matches[0]);
// the output:
array (size=3)
0 => string '__( "brown ")'
1 => string '__('over the')'
2 => string '__("dog")'
And here is example with the same solution on phpliveregex.com:
http://www.phpliveregex.com/p/exF
(section preg_match_all)

Using regex to not match periods between numbers

I have a regex code that splits strings between [.!?], and it works, but I'm trying to add something else to the regex code. I'm trying to make it so that it doesn't match [.] that's between numbers. Is that possible? So, like the example below:
$input = "one.two!three?4.000.";
$inputX = preg_split("~(?>[.!?]+)\K(?!$)~", $input);
print_r($inputX);
Result:
Array ( [0] => one. [1] => two! [2] => three? [3] => 4. [4] => 000. )
Need Result:
Array ( [0] => one. [1] => two! [2] => three? [3] => 4.000. )
You should be able to split on this:
(?<=(?<!\d(?=[.!?]+\d))[.!?])(?![.!?]|$)
https://regex101.com/r/kQ6zO4/1
It uses lookarounds to determine where to split. It looks behind to try to match anything in the set [.!?] one or more times as long as it isn't preceded by and succeeded by a digit.
It also won't return the last empty match by ensuring the last set isn't the end of the string.
UPDATE:
This should be much more efficient actually:
(?!\d+\.\d+).+?[.!?]+\K(?!$)
https://regex101.com/r/eN7rS8/1
Here is another possibility using regex flags:
$input = "one.two!three???4.000.";
$inputX = preg_split("~(\d+\.\d+[.!?]+|.*?[.!?]+)~", $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
print_r($inputX);
It includes the delimiter in the split and ignores empty matches. The regex can be simplified to ((?:\d+\.\d+|.*?)[.!?]+), but I think what is in the code sample above is more efficient.

Regex that recognizes everything except text between quotes?

I need to make a regex that recognizes everything except text between quotes.
Here is an example:
my_var == "Hello world!"
I want to get my_var but not Hello world!.
I tried (?<!\")([A-Za-z0-9]+) but it didn't work.
If you would of took the time to google or search stackoverflow, you would find answers to this question that have already been answered by not only me, but many other users out there.
#Pappa's answer using a negative lookbehind will only match a simple test case and not everything in a string that is not enclosed by quotes. I would suffice for a negative lookahead in this case, if you're wanting to match all word characters in any given data.
/[\w.-]+(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/
See live demo
Example:
<?php
$text = <<<T
my_var == "Hello world!" foo /(^*#&^$
"hello" foobar "hello" FOO "hello" baz
Hi foo, I said "hello" $&#^$(#$)#$&*#(*$&
T;
preg_match_all('/[\w.-]+(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/', $text, $matches);
print_r($matches);
Output
Array
(
[0] => Array
(
[0] => my_var
[1] => foo
[2] => foobar
[3] => FOO
[4] => baz
[5] => Hi
[6] => foo
[7] => I
[8] => said
)
)
You have an accepted answer but I am still submitting once since I believe this answer is better in capturing more edge cases:
$s = 'my_var == "Hello world!" foo';
if (preg_match_all('/[\w.-]+(?=(?:(?:[^"]*"){2})*[^"]*$)/', $s, $arr))
print_r($arr[0]);
OUTPUT:
Array
(
[0] => my_var
[1] => foo
)
This works by using a lookahead to make sure there are even # of double quotes are followed (requires balanced double quotes and no escaping).
As much as I'll regret getting downvoted for answering this, I was intrigued, so did it anyway.
(?<![" a-zA-Z])([A-Za-z0-9\-_\.]+)
This simple solution hasn't been mentioned (see demo):
"[^"]*"(*SKIP)(*F)|[\w.-]+
Reference
How to match pattern except in situations s1, s2, s3

PHP Pattern Modifier: $ for End-of-Lines in Multi-Line Strings

Note: See the bottom of this post for an explanation for why this wasn't originally working.
In PHP, I am attempting to match lower-case characters at the end of every line in a string buffer.
The regex pattern should be [a-z]$. But that only matches the last letter of the string. I believe this a regex modifier issue; I have experimented with /s /m /D, but nothing appears to match as expected.
<?php
$pattern = '/[a-z]$/';
$string = "this
is
a
broken
sentence";
preg_match_all($pattern, $string, $matches);
print_r($matches);
?>
Here's the output:
Array
(
[0] => Array
(
[0] => e
)
)
Here's what I expect the output to be:
Array (
[0] => Array (
[0] => s
[1] => s
[2] => a
[3] => n
[4] => e
)
)
Any advice?
Update: The PHP source code was written on a Windows machine; text editors in Windows, by convention, represent newlines differently than text editors on Unix system.
It appears that the byte-code representation of Windows text files (inheriting from DOS) was not respected by the PHP regex engine. Converting the end-of-line byte-code format to Unix solved the original problem.
Adam Wagner (see below) has posted a pattern that matches regardless of end-of-line byte-representation.
zerkms has the canonical regular expression, to which I am awarding the answer.
$pattern = '/[a-z]$/m';
$string = "this
is
a
broken
sentence";
preg_match_all($pattern, $string, $matches);
print_r($matches);
http://ideone.com/XkeD2
This will return exactly what you want
As #Will points out, it appears you either want the first char of each string, or your example is wrong. If you want the last char of each line (only if it's a lower-case char) you could try this:
/[a-z](?:\n)|[a-z]$/
The first segment [a-z](?:\n), checks to for lowercase chars before newlines. Then [a-z]$ get the last char of the string (in-case it's not followed by a newline.
With your example string, the output is:
Array
(
[0] => Array
(
[0] => s
[1] => a
[2] => n
[3] => e
)
)
Note - The 's' from 'is' is not present because it is followed by a space. To capture this 's' as well (ignoring trailing spaces), you can update the regex to: /[a-z](?:[ ]*\n)|[a-z](?:[ ]*)$/, which checks for 0 or more spaces immediately before the newline (or end of string). Which outputs:
Array
(
[0] => Array
(
[0] => s
[1] => s
[2] => a
[3] => n
[4] => e
)
)
Update
It appears the line-ending style wasn't liking your regex. To account for crazy line-endings (an other unsavory white-space at the end of the lines), you can use this (and still get the /m goodness).
/[a-z](?:\W*)$/m
It looks like you want to match before every newline, not at the end of the file. Perhaps you want
$pattern = '/[a-z]\n/';

Categories