PHP: Parse comma-delimited string outside single and double quotes and parentheses - php

I've found several partial answers to this question, but none that cover all my needs...
I am trying to parse a user generated string as if it were a series of php function arguments to determine the number of arguments:
This string:
$arg1,$arg2='ABC,DEF',$arg3="GHI\",JKL",$arg4=array(1,'2)',"3\"),")
will be inserted as the arguments of a function:
function my_function( [insert string here] ){ ... }
I need to parse the string on the commas, taking into account single- and double-quotes, parentheses, and escaped quotes and parentheses to create an array:
array(4) {
[0] => $arg1
[1] => $arg2='ABC,DEF'
[2] => $arg3="GHI\",JKL"
[3] => $arg4=array(1,'2)',"3\"),")
}
Any help with a regular expression or parser function to accomplish this is appreciated!

It isn't possible to solve this problem with a classical csv tool since there is more than one character able to protect parts of the string.
Using preg_split is possible but will result in a very complicated and inefficient pattern. So the best way is to use preg_match_all. There are however several problems to solve:
as needed, a comma enclosed in quotes or parenthesis must be ignored (seen as a character without special meaning, not as a delimiter)
you need to extract the params, but you need to check if the string has the good format too, otherwise the match results may be totally false!
For the first point, you can define subpatterns to describe each cases: the quoted parts, the parts enclosed between parenthesis, and a more general subpattern able to match a complete param and that uses the two previous subpatterns when needed.
Note that the parenthesis subpattern needs to refer to the general subpattern too, since it can contain anything (and commas too).
The second point can be solved using the \G anchor that ensures that all matchs are contiguous. But you need to be sure that the end of the string has been reached. To do that, you can add an optional empty capture group at the end of the main pattern that is created only if the anchor for the end of the string \z succeeds.
$subject = <<<'EOD'
$arg1,$arg2='ABC,DEF',$arg3="GHI\",JKL",$arg4=array(1,'2)',"3\"),")
EOD;
$pattern = <<<'EOD'
~
# named groups definitions
(?(DEFINE) # this definition group allows to define the subpatterns you want
# without matching anything
(?<quotes>
' [^'\\]*+ (?s:\\.[^'\\]*)*+ ' | " [^"\\]*+ (?s:\\.[^"\\]*)*+ "
)
(?<brackets> \( \g<content> (?: ,+ \g<content> )*+ \) )
(?<content> [^,'"()]*+ # ' # (<-- comment for SO syntax highlighting)
(?:
(?: \g<brackets> | \g<quotes> )
[^,'"()]* # ' #
)*+
)
)
# the main pattern
(?: # two possible beginings
\G(?!\A) , # a comma contiguous to a previous match
| # OR
\A # the start of the string
)
(?<param> \g<content> )
(?: \z (?<check>) )? # create an item "check" when the end is reached
~x
EOD;
$result = false;
if ( preg_match_all($pattern, $subject, $matches, PREG_SET_ORDER) &&
isset(end($matches)['check']) )
$result = array_map(function ($i) { return $i['param']; }, $matches);
else
echo 'bad format' . PHP_EOL;
var_dump($result);
demo

You could split the argument string at ,$ and then append $ back the array values:
$args_array = explode(',$', $arg_str);
foreach($args_array as $key => $arg_raw) {
$args_array[$key] = '$'.ltrim($arg_raw, '$');
}
print_r($args_array);
Output:
(
[0] => $arg1
[1] => $arg2='ABC,DEF'
[2] => $arg3="GHI\",JKL"
[3] => $arg4=array(1,'2)',"3\"),")
)

If you want to use a regex, you can use something like this:
(.+?)(?:,(?=\$)|$)
Working demo
Php code:
$re = '/(.+?)(?:,(?=\$)|$)/';
$str = "\$arg1,\$arg2='ABC,DEF',\$arg3=\"GHI\",JKL\",\$arg4=array(1,'2)',\"3\"),\")\n";
preg_match_all($re, $str, $matches);
Match information:
MATCH 1
1. [0-5] `$arg1`
MATCH 2
1. [6-21] `$arg2='ABC,DEF'`
MATCH 3
1. [22-39] `$arg3="GHI\",JKL"`
MATCH 4
1. [40-67] `$arg4=array(1,'2)',"3\"),")`

Related

Splitting by a semicolon not surrounded by quote signs

Well, hello community. I'm workin' on a CSV decoder in PHP (yeah, I know there's already one, but as a challenge for me, since I'm learning it in my free time). Now the problem: Well, the rows are split up by PHP_EOL.
In this line:
foreach(explode($sep, $str) as $line) {
where sep is the variable which splits up the rows and str the string I wanna decode.
But if I wanna split up the columns by a semicolon there might be a situation where a semicolon is content of one column. And as I researched this problem is solved by surrounding the whole column by quote signs like this:
Input:
"0;0";1;2;3;4
Expected output:
0;0 | 1 | 2 | 3 | 4
I already thought of lookahead/lookbehind. But as I didn't use it in past and maybe this could be a good practice for it I don't know how to include it in the regex. My decoding function returns a 2D-array (like a table...) and I thought of adding rows to the array like this (Yep, the regex is f***ed up...):
$res[] = preg_split("/(?<!\")". preg_quote($delim). "(?!\")/", $line);
And at last my full code:
function csv_decode($str, $delim = ";", $sep = PHP_EOL) {
if($delim == "\"") $delim = ";";
$res = [];
foreach(explode($sep, $str) as $line) {
$res[] = preg_split("/(?<!\")". preg_quote($delim). "(?!\")/", $line);
}
return $res;
}
Thanks in advance!
It's a bit counter-intuitive, but the simplest way to split a string by regex is often to use preg_match_all in place of preg_split:
preg_match_all('~("[^"]*"|[^;"]*)(?:;|$)~A', $line, $m);
$res[] = $m[1];
The A modifier ensures the contiguity of the successive matches from the start of the string.
If you don't want the quotes to be included in the result, you can use the branch reset feature (?|..(..)..|..(..)..):
preg_match_all('~(?|"([^"]*)"|([^;"]*))(?:;|$)~A', $line, $m);
Other workaround, but this time for preg_split: include the part you want to avoid before the delimiter and discard it from the whole match using the \K feature:
$res[] = preg_split('~(?:"[^"]*")?\K;~', $line);
You can use this function str_getcsv in this you can specify a custom delimiter(;) as well.
Try this code snippet
<?php
$string='"0;0";1;2;3;4';
print_r(str_getcsv($string,";"));
Output:
Array
(
[0] => 0;0
[1] => 1
[2] => 2
[3] => 3
[4] => 4
)
Split is not a good choice for csv type lines.
You could use the old tried and true \G anchor with a find globally type func.
Practical
Regex: '~\G(?:(?:^|;)\s*)(?|"([^"]*)"|([^;]*?))(?:\s*(?:(?=;)|$))~'
Info:
\G # G anchor, start where last match left off
(?: # leading BOL or ;
(?: ^ | ; )
\s* # optional whitespaces
)
(?| # branch reset
"
( [^"]* ) # (1), double quoted string data
"
| # or
( [^;]*? ) # (1), non-quoted field
)
(?: # trailing optional whitespaces
\s*
(?:
(?= ; ) # lookahead for ;
| $ # or EOL
)
)

preg_split shortcode attributes into array

I would like to parse shortcode into array via "preg_split".
This is example shortcode:
[contactform id="8411" label="This is \" first label" label2='This is second \' label']
and this should be result array:
Array
(
[id] => 8411
[label] => This is \" first label
[label2] => This is second \' label
)
I have this regexp:
$atts_arr = preg_split('~\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)~', trim($shortcode, '[]'));
Unfortunately, this works only if there is no escaping of quotes \' or \".
Thx in advance!
Using preg_split is not always handy or appropriate in particular when you have to deal with escaped quotes. So, a better approach consists to use preg_match_all, example:
$pattern = <<<'EOD'
~
(\w+) \s*=
(?|
\s* "([^"\\]*(?:\\.[^"\\]*)*)"
|
\s* '([^'\\]*(?:\\.[^'\\]*)*)'
# | uncomment if you want to handle unquoted attributes
# ([^]\s]*)
)
~xs
EOD;
if (preg_match_all($pattern, $yourshortcode, $matches))
$attributes = array_combine($matches[1], $matches[2]);
The pattern uses the branch reset feature (?|...(..)...|...(...)..) that gives the same number(s) to the capture groups for each branch.
I was speaking about the \G anchor in my comment, this anchor succeeds if the current position is immediatly after the last match. It can be useful if you want to check the syntax of your shortcode from start to end at the same time (otherwise it is totally useless). Example:
$pattern2 = <<<'EOD'
~
(?:
\G(?!\A) # anchor for the position after the last match
# it ensures that all matches are contiguous
|
\[(?<tagName>\w+) # begining of the shortcode
)
\s+
(?<key>\w+) \s*=
(?|
\s* "(?<value>[^"\\]*(?:\\.[^"\\]*)*)"
|
\s* '([^'\\]*(?:\\.[^'\\]*)*')
# | uncomment if you want to handle unquoted attributes
# ([^]\s]*)
)
(?<end>\s*+]\z)? # check that the end has been reached
~xs
EOD;
if (preg_match_all($pattern2, $yourshortcode, $matches) && isset($matches['end']))
$attributes = array_combine($matches['key'], $matches['value']);

Negation of a string in a regex

I realise that something similar has been asked before, but I can't seem to fit the solution to what I am trying to do, so please don't just think this is a dupe.
I have a string in the style {block:string}contents{/block:string}, which can be matched fairly easily with {block:([a-z_-\s]+)}.*{/block:\1}
What I want to do is modify the inner .* part so that it does not match any string that has a {block:[a-z_-\s]+} between it, that is all {block}{/block} that have a {block} inside them should not be matched.
Thanks!
Try
{block:([a-z_-\s]+)}[^{]*(?!{block:([a-z_-\s]+)}.*{\block:\2})[^}]*{/block:\1}
I am pretty mediocre at regex, but the negative lookahead bounded by the [^{]* and [^}]* statements should keep your matches tag-free.
Compressed: m~\{block:([a-z\s_-]+)\}(?:(?!\{/?block:\1\}).)*\{/block:\1\}~xs
Example in Perl:
$_ = '{block:string}conte{block:string}nts{/block:string}{/block:string}';
if ( m~ # match operator
\{block: ([a-z\s_-]+) \} # opening block structure and capt grp 1
(?: # begin non capt grp
(?! \{/?block: \1 \} ) # negative lookahead, don't want backreffed
# open or closed block struct
. # ok, grab this character
)* # end group, do 0 or more times (greedy)
\{/block: \1 \} # closing block structure matching grp 1
~xs ) # modifiers: expanded, include newlines
{
print "matched '$&'\n";
}
Output:
matched '{block:string}nts{/block:string}'
<?php
$ptn = "%(?:{block:[a-z_\s-]+})(?![^}]*?{block:).*?{/block:[a-z_\s-]+}%";
$str = "... your content here ...";
preg_match_all($ptn, $str, $matches);
print_r($matches);
?>
For example:
$str = "{block:string}test2{/block:string} {block:string}contents{block:string}{block:string}test3{/block:string}{/block:string}{/block:string} sdf ";
Would produce:
Array
(
[0] => Array
(
[0] => {block:string}test2{/block:string}
[1] => {block:string}test3{/block:string}
)
)

PHP: Get last Tag of a String with Regular Expressions

Quite simple problem (but difficult solution): I got a string in PHP like as follows:
['one']['two']['three']
And from this, i must extract the last tags, so i finally got three
it is also possible that there is a number, like
[1][2][3]
and then i must get 3
How can i solve this?
Thanks for your help!
Flo
Your tag is \[[^\]]+\].
3 Tags are: (\[[^\]]+\]){3}
3 Tags at end are: (\[[^\]]+\]){3}$
N Tags at end are: (\[[^\]]+\])*$ (N 0..n)
Example:
<?php
$string = "['one']['two']['three'][1][2][3]['last']";
preg_match("/((?:\[[^\]+]*\]){3})$/", $string, $match);
print_r($match); // Array ( [0] => [2][3]['last'] [1] => [2][3]['last'] )
This tested code may work for you:
function getLastTag($text) {
$re = '/
# Match contents of last [Tag].
\[ # Literal start of last tag.
(?: # Group tag contents alternatives.
\'([^\']+)\' # Either $1: single quoted,
| (\d+) # or $2: un-quoted digits.
) # End group of tag contents alts.
\] # Literal end of last tag.
\s* # Allow trailing whitespace.
$ # Anchor to end of string.
/x';
if (preg_match($re, $text, $matches)) {
if ($matches[1]) return $matches[1]; // Either single quoted,
if ($matches[2]) return $matches[2]; // or non quoted digit.
}
return null; // No match. Return NULL.
}
Here is a regex that may work for you. Try this:
[^\[\]']*(?='?\]$)

How do I match nested braces using regular expressions in PHP?

I have an LaTeX document I want to match. And I need a RegEx match that matches the following:
\ # the backslash in the beginning
[a-zA-Z]+ #a word
(\{.+\})* # any amount of {something}
However, and her is the catch;
In the last line, it 1. needs to be greedy and 2. needs to have a matching number of {} inside itself.
Meaning if I have the string \test{something\somthing{9}}
it would match the whole. And it needs to be in that order ({}). So that it doesn't match the following:
\LaTeX{} is a document preparation system for the \TeX{}
just
\LaTeX{}
and
\TeX{}
Help anyone? Maybe someone have an better idea for matching? Should I not use regular expressions?
This can be done with recursion:
$input = "\LaTeX{} is a document preparation system for the \TeX{}
\latex{something\somthing{9}}";
preg_match_all('~(?<token>
\\\\ # the slash in the beginning
[a-zA-Z]+ #a word
(\{[^{}]*((?P>token)[^{}]*)?\}) # {something}
)~x', $input, $matches);
This correctly matches \LaTeX{}, \TeX{}, and \latex{something\somthing{9}}
PHP could be used since it supports recursive regex-matching. But, as I said, if you have comments in your LaTeX-like strings that can have { or } in them, this will fail.
A demo:
$text = 'This is a \LaTeX{ foo { bar { ... } baz test {} done } } document
preparation system for the \TeX{a{b{c}d}e{f}g{h}i}-y people out there';
preg_match_all('/\\\\[A-Za-z]+(\{(?:[^{}]|(?1))*})/', $text, $matches, PREG_SET_ORDER);
print_r($matches);
which produces:
Array
(
[0] => Array
(
[0] => \LaTeX{ foo { bar { ... } baz test {} done } }
[1] => { foo { bar { ... } baz test {} done } }
)
[1] => Array
(
[0] => \TeX{a{b{c}d}e{f}g{h}i}
[1] => {a{b{c}d}e{f}g{h}i}
)
)
A quick explanation:
\\\\ # the literal '\'
[A-Za-z]+ # one or more letters
( # start capture group 1 <-----------------+
\{ # the literal '{' |
(?: # start non-capture group A |
[^{}] # any character other than '{' and '}' |
| # OR |
(?1) # recursively match capture group 1 ---+
) # end non-capture group A
* # non-capture group A zero or more times
} # the literal '}'
) # end capture group 1
Unfortunately, I believe this is impossible. Bracket matching (detecting properly paired, nested brackets) is commonly used as an example of a problem that cannot be solved with a finite state machine, such as a regular expression parser. You could do it with a context free grammar, but that's just not how regex works. Your best solution is to use a regex like {*[^{}]*}* for the initial check, and then another short script to check whether it's an even number.
In conclusion: don't try and do it with only regex. This is not a problem that can be solved with regex alone.

Categories