Regular expression to parse pipe-delimited data enclosed in double braces - php

I'm trying to match a string like this:
{{name|arg1|arg2|...|argX}}
with a regular expression
I'm using preg_match with
/{{(\w+)\|(\w+)(?:\|(.+))*}}/
but I get something like this, whenever I use more than two args
Array
(
[0] => {{name|arg1|arg2|arg3|arg4}}
[1] => name
[2] => arg1
[3] => arg2|arg3|arg4
)
The first two items cannot contain spaces, the rest can.
Perhaps I'm working too long on this, but I can't find the error - any help would be greatly appreciated.
Thanks Jan

Don't use regular expressions for these kind of simple tasks. What you really need is:
$inner = substr($string, 2, -2);
$parts = explode('|', $inner);
# And if you want to make sure the string has opening/closing braces:
$length = strlen($string);
assert($inner[0] === '{');
assert($inner[1] === '{');
assert($inner[$length - 1] === '}');
assert($inner[$length - 2] === '}');

The problem is here: \|(.+)
Regular expressions, by default, match as many characters as possible. Since . is any character, other instances of | are happily matched too, which is not what you would like.
To prevent this, you should exclude | from the expression, saying "match anything except |", resulting in \|([^\|]+).

Should work for anywhere from 1 to N arguments
<?php
$pattern = "/^\{\{([a-z]+)(?:\}\}$|(?:\|([a-z]+))(?:\|([a-z ]+))*\}\}$)/i";
$tests = array(
"{{name}}" // should pass
, "{{name|argOne}}" // should pass
, "{{name|argOne|arg Two}}" // should pass
, "{{name|argOne|arg Two|arg Three}}" // should pass
, "{{na me}}" // should fail
, "{{name|arg One}}" // should fail
, "{{name|arg One|arg Two}}" // should fail
, "{{name|argOne|arg Two|arg3}}" // should fail
);
foreach ( $tests as $test )
{
if ( preg_match( $pattern, $test, $matches ) )
{
echo $test, ': Matched!<pre>', print_r( $matches, 1 ), '</pre>';
} else {
echo $test, ': Did not match =(<br>';
}
}

Of course you would get something like this :) There is no way in regular expression to return dynamic count of matches - in your case the arguments.
Looking at what you want to do, you should keep up with the current regular expression and just explode the extra args by '|' and add them to an args array.

indeed, this is from PCRE manual:
When a capturing subpattern is
repeated, the value captured is the
substring that matched the final
iteration. For example, after
(tweedle[dume]{3}\s*)+ has matched
"tweedledum tweedledee" the value of
the captured substring is
"tweedledee". However, if there are
nested capturing subpatterns, the
corresponding captured values may have
been set in previous iterations. For
example, after /(a|(b))+/ matches
"aba" the value of the second captured
substring is "b".

Related

Trying to create a regex in PHP that matches patterns inside a pattern

I have seen some regex examples where the string is "Test string: Group1Group2", and using preg_match_all(), matching for patterns of text that exists inside the tags.
However, what I am trying to do is a bit different, where my string is something like this:
"some t3xt../s8fo=123,sij(variable1=123,variable2=743,variable3=535)"
What I want to do is match the sections such as 'variable=123' that exist inside the parenthesis.
What I have so far is this:
if( preg_match_all("/\(([^\)]*?)\)"), $string_value, $matches )
{
print_r( $matches[1] );
}
But this just captures everything that's inside the parenthesis, and doesn't match anything else.
Edit:
The desired output would be:
"variable1=123"
"variable2=743"
"variable3=535"
The output that I am getting is:
"variable1=123,variable2=743,variable3=535"
You can extract the matches you need with a single call to preg_match_all if the matches do not contain (, ) or ,:
$s = '"some t3xt../s8fo=123,sij(variable1=123,variable2=743,variable3=535)"';
if (preg_match_all('~(?:\G(?!\A),|\()\K[^,]+(?=[^()]*\))~', $s, $matches)) {
print_r($matches[0]);
}
See the regex demo and a PHP demo.
Details:
(?:\G(?!\A),|\() - either end of the preceding successful match and a comma, or a ( char
\K - match reset operator that discards all text matched so far from the current overall match memory buffer
[^,]+ - one or more chars other than a comma (use [^,]* if you expect empty matches, too)
(?=[^()]*\)) - a positive lookahead that requires zero or more chars other than ( and ) and then a ) immediately to the right of the current location.
I would do this:
preg_match("/\(([^\)]+)\)/", $string_value, $matches);
$result = explode(",", $matches[1]);
If your end result is an array of key => value then you can transform it into a query string:
preg_match("/\(([^\)]+)\)/", $string_value, $matches);
parse_str(str_replace(',', '&', $matches[1]), $result);
Which yields:
Array
(
[variable1] => 123
[variable2] => 743
[variable3] => 535
)
Or replace with a newline \n and use parse_ini_string().

How to separate string to number in single word with PHP?

I have the word AK747, I use regex to detect if a string (at least 2 chars ex: AK) is followed by a number (at least to digits ex: 747).
EDIT : (sorry that I wasn't clear on this guys)
I need to do this above because :
In some case I need to split to match search against AK-747. When I search for string 'AK-747' with keyword 'AK747' it won't find a match unless I use levenshtein in database, so I prefer splitting AK747 to AK and 747.
My code:
$strNumMatch = preg_match('/^[a-zA-Z]{2,}[0-9]{2,}$/',
$value, $match);
if(isset($match[0]))
echo $match[0];
How do I split to array ['AK', '747'] for example with preg_split() or any other way?
$input = 'AK-747';
if (preg_match('/^([a-z]{2,})-?([0-9]{2,})$/i', $input, $result)) {
unset($result[0]);
}
print_r($result);
The output:
Array
(
[1] => AK
[2] => 747
)
You may try this:
preg_match('/[0-9]{2,}/', $value, $matches, PREG_OFFSET_CAPTURE);
$position = $matches[0][1];
$letters = substr($value, 0, $position);
$numbers = substr($value, $position);
This way you get the position of the first number and split there.
EDIT:
Starting from your original approach this could look somewhat like this:
$strNumMatch = preg_match('/^([a-zA-Z]{2,})([0-9]{2,})$/', $value, $match, PREG_OFFSET_CAPTURE);
if($strNumMatch){
$position = $matches[2][1];
$letters = substr($value, 0, $position);
$numbers = substr($value, $position);
$alternative = $letters.'-'.$numbers;
}
preg_split() is a very sensible and direct call since you desire an indexed array containing the two substrings.
Code: (Demo)
$input = 'AK-747';
var_export(preg_split('/[a-z]{2,}\K-?/i',$input));
Output:
array (
0 => 'AK',
1 => '747',
)
The \K means "restart the fullstring match". Effectively, everything to the left of \K is retained as the first element in the result array and everything to right (the optional hyphen) is omitted because it is considered the delimiter. Pattern Demo
Code: (Demo)
I process a small battery of inputs to show what can be done and explain after the snippet.
$inputs=['AK747','AK-747','AK-','AK']; // variations as I understand them
foreach($inputs as $input){
echo "$input returns: ";
var_export(preg_split('/[a-z]{2,}\K-?/i',$input,2,PREG_SPLIT_NO_EMPTY));
echo "\n";
}
Output:
AK747 returns: array (
0 => 'AK',
1 => '747',
)
AK-747 returns: array (
0 => 'AK',
1 => '747',
)
AK- returns: array (
0 => 'AK',
)
AK returns: array (
0 => 'AK',
)
preg_split() takes a pattern that receives a pattern that will match a variable substring and use it as a delimiter. If - were present in every input string then explode('-',$input) would be most appropriate. However, - is optional in this task, so the pattern must allow - to be optional (this is what the ? quantifier does in all of the patterns on this page).
Now, you couldn't just use a pattern like /-?/, that would split the string on every character. To overcome this, you need to tell the regex engine the exact expected location for the optional -. You do this by referencing [a-z]{2,} before the -? (single intended delimiter).
The pattern /[a-z]{2,}-?/i does a fair job of finding the correct location for the optional hyphen, but now the trouble is, the leading letters in the string are included as part of the delimiting substring.
Sometimes, "lookarounds" can be used in regex patterns to match but not consume substrings. A "positive lookbehind" is used to match a preceding substring, however "variable length lookbehinds" are not permitted in php (and most other regex flavors). This is what the invalid pattern would look like: /(?<=[a-z]{2,})-?/i.
The way around this technicality is to "restart the fullstring match" using the \K token (aka a lookbehind alternative) just before the optional hyphen. To correctly target only the intended delimiter, the leading letters must be "matched/consumed" then "discarded" -- that's what \K does.
As for the inclusion of the 3rd and 4th parameter of preg_split()...
I've set the 3rd parameter to 2. This is just like the limit parameter that explode() has. It instructs the function to not make more than 2 output elements. For this case, I could have used NULL or -1 to mean "unlimited", but I could NOT leave the parameter empty -- it must be assigned to allow for the declaration of the 4th parameter.
I've set the 4th parameter to PREG_SPLIT_NO_EMPTY which instructs the function to not generate empty output elements.
Ta-Da!
p.s. a preg_match_all() solution is as easy as using a pipe and two anchors:
$inputs=['AK747','AK-747','AK-','AK']; // variations as I understand them
foreach($inputs as $input){
echo "$input returns: ";
var_export(preg_match_all('/^[a-z]{2,}|\d{2,}$/i',$input,$out)?$out[0]:[]);
echo "\n";
}
// same outputs as above
You can make the - optional with ?.
/([A-Za-z]{2,}-?[0-9]{2,})/
https://regex101.com/r/tIgM4F/1

RegEx Named Capturing Groups in PHP

I have the following regex to capture a list of numbers (it will be more complex than this eventually):
$list = '10,9,8,7,6,5,4,3,2,1';
$regex =
<<<REGEX
/(?x)
(?(DEFINE)
(?<number> (\d+) )
(?<list> (?&number)(,(?&number))* )
)
^(?&list)/
REGEX;
$matches = array();
if (preg_match($regex,$list,$matches)==1) {
print_r($matches);
}
Which outputs:
Array ( [0] => 10,9,8,7,6,5,4,3,2,1 )
How do I capture the individual numbers in the list in the $matches array? I don't seem to be able to do it, despite putting a capturing group around the digits (\d+).
EDIT
Just to make it clearer, I want to eventually use recursion, so explode is not ideal:
$match =
<<<REGEX
/(?x)
(?(DEFINE)
(?<number> (\d+) )
(?<member> (?&number)|(?&list) )
(?<list> \( ((?&number)|(?&member))(,(?&member))* \) )
)
^(?&list)/
REGEX;
The purpose of a (?(DEFINE)...) section is only to define named sub-patterns you can use later in the define section itself or in the main pattern. Since these sub-patterns are not defined in the main pattern they don't capture anything, and a reference (?&number) is only a kind of alias for the sub-pattern \d+ and doesn't capture anything too.
Example with the string: 1abcde2
If I use this pattern: /^(?<num>\d).....(?&num)$/ only 1 is captured in the group num, (?&num) doesn't capture anything, it's only an alias for \d./^(?<num>\d).....\d$/ produces exactly the same result.
An other point to clarify. With PCRE (the PHP regex engine), a capture group (named or not) can only store one value, even if you repeat it.
The main problem of your approach is that you are trying to do two things at the same time:
you want to check the format of the string.
you want to extract an unknown number of items.
Doing this is only possible in particular situations, but impossible in general.
For example, with a flat list like: $list = '10,9,8,7,6,5,4,3,2,1'; where there are no nested elements, you can use a function like preg_match_all to reuse the same pattern several times in this way:
if (preg_match_all('~\G(\d+)(,|$)~', $list, $matches) && !end($matches[2])) {
// \G ensures that results are contiguous
// you have all the items in $matches[1]
// if the last item of $matches[2] is empty, this means
// that the end of the string is reached and the string
// format is correct
echo '<°)))))))>';
}
Now if you have a nested list like $list = '10,9,(8,(7,6),5),4,(3,2),1'; and you want for example to check the format and to produce a tree structure like:
[ 10, 9, [ 8, [ 7, 6 ], 5 ], 4 , [ 3, 2 ], 1 ]
You can't do it with a single pass. You need one pattern to check the whole string format and an other pattern to extract elements (and a recursive function to use it).
<<<FORGET_THIS_IMMEDIATELY
As an aside you can do it with eval and strtr, but it's a very dirty and dangerous way:
eval('$result=[' . strtr($list, '()', '[]') . '];');
FORGET_THIS_IMMEDIATELY;
If you mean to get an array of the comma delimited numbers, then explode:
$numbers = explode(',', $matches[0]); //first parameter is your delimiter what the string will be split up by. And the second parameter is the initial string
print_r($numbers);
output:
Array(
[0] => 10,
[1] => 9,
[2] => 8,
etc
For this simple list, this would be enough (if you have to use a regular expression):
$string = '10,9,8,7,6,5,4,3,2,1';
$pattern = '/([\d]+),?/';
preg_match_all($pattern, $string, $matches);
print_r($matches[1]);

find a specific word in string php

I have a text in PHP stored in the variable $row. I'd like to find the position of a certain group of words and that's quite easy. What's not so easy is to make my code recognize that the word it has found is exactly the word i'm looking for or a part of a larger word. Is there a way to do it?
Example of what I'd like to obtain
CODE:
$row= "some ugly text of some kind i'd like to find in someway"
$token= "some";
$pos= -1;
$counter= substr_count($row, $token);
for ($h=0; $h<$counter; $h++) {
$pos= strpos($row, $token, $pos+1);
echo $pos.' ';
}
OUTPUT:
what I obtain:
0 17 47
what I'd like to obtain
0 17
Any hint?
Use preg_match_all() with word boundaries (\b):
$search = preg_quote($token, '/');
preg_match_all("/\b$search\b/", $row, $m, PREG_OFFSET_CAPTURE);
Here, the preg_quote() statement is used to correctly escape the user input so as to use it in our regular expression. Some characters have special meaning in regular expression language — without proper escaping, those characters will lose their "special meaning" and your regex might not work as intended.
In the preg_match_all() statement, we are supplying the following regex:
/\b$search\b/
Explanation:
/ - starting delimiter
\b - word boundary. A word boundary, in most regex dialects, is a position between a word character (\w) and a non-word character (\W).
$search - escaped search term
\b - word boundary
/ - ending delimiter
In simple English, it means: find all the occurrences of the given word some.
Note that we're also using PREG_OFFSET_CAPTURE flag here. If this flag is passed, for every occurring match the appendant string offset will also be returned. See the documentation for more information.
To obtain the results you want, you can simply loop through the $m array and extract the offsets:
$result = implode(' ', array_map(function($arr) {
return $arr[1];
}, $m[0]));
echo $result;
Output:
0 18
Demo
What you're looking for is a combination of Regex with a word boundaries pattern and the flag to return the offset (PREG_OFFSET_CAPTURE).
PREG_OFFSET_CAPTURE
If this flag is passed, for every occurring match the appendant
string offset will also be returned. Note that this changes the
value of matches into an array where every element is an array
consisting of the matched string at offset 0 and its string offset
into subject at offset 1.
$row= "some ugly text of some kind i'd like to find in someway";
$pattern= "/\bsome\b/i";
preg_match_all($pattern, $row, $matches, PREG_OFFSET_CAPTURE);
And we get something like this:
Array
(
[0] => Array
(
[0] => Array
(
[0] => some
[1] => 0
)
[1] => Array
(
[0] => some
[1] => 18
)
)
)
And just loop through the matches and extract the offset where the needle was found in the haystack.
// store the positions of the match
$offsets = array();
foreach($matches[0] as $match) {
$offsets[] = $match[1];
}
// display the offsets
echo implode(' ', $offsets);
Use preg_match():
if(preg_match("/some/", $row))
// [..]
The first argument is a regex, which can match virtually anything you want to match. But, there are dire warnings about using it to match things like HTML.

Manipulating string using PHP

In PHP, I have strings like so:
$string = "This is a 123 test (your) string (for example 1234) to test.";
From that string, I'd like to get the words inside the () with the numbers. I've tried using explode but since I have 2 word/group of strings enclosed in the parentheses, I end up getting (your) instead of (for example 1234). I've also used substr like so:
substr($string, -20)
This works most of the time but the problem with this is, there are instances that the string is shorter so it ends up getting even the unwanted string. I've also tried using regular expression in which I set something like so:
/[^for]/
but that did not work either. The string I want to get always starts with "for" but the length varies. How do I manipulate php so that I can get only the string enclosed inside the parentheses that starts with the word for?
I might use preg_match() in this case.
preg_match("#\((for.*?)\)#",$string,$matches);
Any matches found would be stored in $matches.
Use the following regular expression:
(\(for.*?\))
It will capture patterns like:
(for)
(foremost)
(for example)
(for 1)
(for: 1000)
A sample PHP code:
$pattern = '/(\(for.*?\))/';
$result = preg_match_all(
$pattern,
" text (for example 1000) words (for: 20) other words",
$matches
);
if ( $result > 0 ) {
print_r( $matches );
}
Above print_r( $matches ) result:
Array
(
[0] => Array
(
[0] => (for example 1000)
[1] => (for: 20)
)
[1] => Array
(
[0] => (for example 1000)
[1] => (for: 20)
)
)
Use preg_match for regular expression
$matches = array();
$pattern = '/^for/i';
preg_match($pattern,$string,$matches);
pirnt_r($matches);
If matches is provided, then it is filled with the results of search. $matches[0] will contain the text that matched the full pattern, $matches[1] will have the text that matched the first captured parenthesized subpattern, and so on.
$matches = array();
preg_match("/(\(for[\w\d\s]+\))/i",$string,$matches);
var_dump($matches);

Categories