I have the following in an INI file:
[country]
SE = Sweden
NO = Norway
FI = Finland
However, when var_dump()ing PHP's parse_ini_file() function, I get the following output:
PHP Warning: syntax error, unexpected BOOL_FALSE in test.ini on line 2
in /Users/andrew/sandbox/test.php on line 1
bool(false)
It appears that "NO" is reserved. Is there any other way I can set a variable named "NO"?
Another hack would be to reverse your ini keys with their values and use array_flip:
<?php
$ini =
"
[country]
Sweden = 'SE'
Norway = 'NO'
Finland = 'FI'
";
$countries = parse_ini_string($ini, true);
$countries = array_flip($countries["country"]);
echo $countries["NO"];
Still you will need to use quotes around NO (at least), if you do
Norway = NO
you don't get an error but value for $countries["NO"] will be an empty string.
This propably comes a little late but the way PHPs parse_ini_file works bothered me so much that I wrote my own little parser.
Feel free to use it, but use with care it has only been shallowly tested!
// the exception used by the parser
class IniParserException extends \Exception {
public function __construct($message, $code = 0, \Exception $previous = null) {
parent::__construct($message, $code, $previous);
}
public function __toString() {
return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
}
// the parser
function my_parse_ini_file($filename, $processSections = false) {
$initext = file_get_contents($filename);
$ret = [];
$section = null;
$lineNum = 0;
$lines = explode("\n", str_replace("\r\n", "\n", $initext));
foreach($lines as $line) {
++$lineNum;
$line = trim(preg_replace('/[;#].*/', '', $line));
if(strlen($line) === 0) {
continue;
}
if($processSections && $line{0} === '[' && $line{strlen($line)-1} === ']') {
// section header
$section = trim(substr($line, 1, -1));
} else {
$eqIndex = strpos($line, '=');
if($eqIndex !== false) {
$key = trim(substr($line, 0, $eqIndex));
$matches = [];
preg_match('/(?<name>\w+)(?<index>\[\w*\])?/', $key, $matches);
if(!array_key_exists('name', $matches)) {
throw new IniParserException("Variable name must not be empty! In file \"$filename\" in line $lineNum.");
}
$keyName = $matches['name'];
if(array_key_exists('index', $matches)) {
$isArray = true;
$arrayIndex = trim($matches['index']);
if(strlen($arrayIndex) == 0) {
$arrayIndex = null;
}
} else {
$isArray = false;
$arrayIndex = null;
}
$value = trim(substr($line, $eqIndex+1));
if($value{0} === '"' && $value{strlen($value)-1} === '"') {
// too lazy to check for multiple closing " let's assume it's fine
$value = str_replace('\\"', '"', substr($value, 1, -1));
} else {
// special value
switch(strtolower($value)) {
case 'yes':
case 'true':
case 'on':
$value = true;
break;
case 'no':
case 'false':
case 'off':
$value = false;
break;
case 'null':
case 'none':
$value = null;
break;
default:
if(is_numeric($value)) {
$value = $value + 0; // make it an int/float
} else {
throw new IniParserException("\"$value\" is not a valid value! In file \"$filename\" in line $lineNum.");
}
}
}
if($section !== null) {
if($isArray) {
if(!array_key_exists($keyName, $ret[$section])) {
$ret[$section][$keyName] = [];
}
if($arrayIndex === null) {
$ret[$section][$keyName][] = $value;
} else {
$ret[$section][$keyName][$arrayIndex] = $value;
}
} else {
$ret[$section][$keyName] = $value;
}
} else {
if($isArray) {
if(!array_key_exists($keyName, $ret)) {
$ret[$keyName] = [];
}
if($arrayIndex === null) {
$ret[$keyName][] = $value;
} else {
$ret[$keyName][$arrayIndex] = $value;
}
} else {
$ret[$keyName] = $value;
}
}
}
}
}
return $ret;
}
What does it differently? Variable names may only consist of alphanumerical characters but other than that no restrictions to them. Strings must be encapsulated with " everything else has to be a special value like no, yes, true, false, on, off, null or none. For mapping see code.
Kind of a hack but you can add backticks around the key names:
[country]
`SE` = Sweden
`NO` = Norway
`FI` = Finland
Then access them like so:
$result = parse_ini_file('test.ini');
echo "{$result['`NO`']}\n";
Output:
$ php test.php
Norway
I was getting this error when there were single quote combinations in the string such as 't or 's. To get rid of the problem, I wrapped the string in double quotes:
Before:
You have selected 'Yes' but you haven't entered the date's flexibility
After:
"You have selected 'Yes' but you haven't entered the date's flexibility"
I ran into the same problem and tried to escape the name in every possible way.
Then I remembered that because of the INI syntax both names and values will be trimmed, so the following workaround MAYBE should do the trick:
NL = Netherlands
; A whitespace before the name
NO = Norway
PL = Poland
And it works ;) As long as your co-workers read the comments (which is not always the case) and don't delete it accidentally. So, yes, the array flipping solution is a safe bet.
From the manual page for parse_ini_file:
There are reserved words which must not be used as keys for ini files. These include: null, yes, no, true, false, on, off, none.
So no, you can't set a variable NO.
Related
We have got web app which does replacing some text with another using str_replace().
Find strings and replace strings are stored in template file.
We what to replace str_replace() function to preg_replace() to have possibility to use regex in find strings (to set them in the same template file).
In original scripts we have such parts of php code.
In one file:
class SiteConfig {
// Strings to search for in HTML before processing begins (used with $replace_string)
public $find_string = array();
// Strings to replace those found in $find_string before HTML processing begins
public $replace_string = array();
// a lot of code goes here
public function append(SiteConfig $newconfig) {
foreach (array('find_string', 'replace_string') as $var) {
// append array elements for this config variable from $newconfig to this config
//$this->$var = $this->$var + $newconfig->$var;
$this->$var = array_merge($this->$var, $newconfig->$var);
}
}
// a lot of code goes here
public static function build_from_array(array $lines) {
$config = new SiteConfig();
foreach ($lines as $line) {
$line = trim($line);
// skip comments, empty lines
if ($line == '' || $line[0] == '#') continue;
// get command
$command = explode(':', $line, 2);
// if there's no colon ':', skip this line
if (count($command) != 2) continue;
$val = trim($command[1]);
$command = trim($command[0]);
//if ($command == '' || $val == '') continue;
// $val can be empty, e.g. replace_string:
if ($command == '') continue;
// strip_attr is now an alias for strip.
// In FTR 3.8 we can strip attributes from elements, not only the elements themselves
// e.g. strip: //img/#srcset (removes srcset attribute from all img elements)
// but for backward compatibility (to avoid errors with new config files + old version of FTR)
// we've introduced strip_attr and we'll recommend using that in our public site config rep.
// strip_attr: //img/#srcset
if ($command == 'strip_attr') $command = 'strip';
// check for commands where we accept multiple statements
if (in_array($command, array('title', 'body', 'author', 'date', 'strip', 'strip_id_or_class', 'strip_image_src', 'single_page_link', 'single_page_link_in_feed', 'next_page_link', 'native_ad_clue', 'http_header', 'test_url', 'find_string', 'replace_string'))) {
array_push($config->$command, $val);
// check for single statement commands that evaluate to true or false
} elseif (in_array($command, array('tidy', 'prune', 'autodetect_on_failure', 'insert_detected_image'))) {
$config->$command = ($val == 'yes');
// check for single statement commands stored as strings
} elseif (in_array($command, array('parser'))) {
$config->$command = $val;
// special treatment for test_contains
} elseif (in_array($command, array('test_contains'))) {
$config->add_test_contains($val);
// special treatment for if_page_contains
} elseif (in_array($command, array('if_page_contains'))) {
$config->add_if_page_contains_condition($val);
// check for replace_string(find): replace
} elseif ((substr($command, -1) == ')') && preg_match('!^([a-z0-9_]+)\((.*?)\)$!i', $command, $match)) {
if (in_array($match[1], array('replace_string'))) {
array_push($config->find_string, $match[2]);
array_push($config->replace_string, $val);
} elseif (in_array($match[1], array('http_header'))) {
$_header = strtolower(trim($match[2]));
$config->http_header[$_header] = $val;
}
}
}
return $config;
}
}
In another file:
public function process($html, $url, $smart_tidy=true) {
// a lot of code goes before
// do string replacements
if (!empty($this->config->find_string)) {
if (count($this->config->find_string) == count($this->config->replace_string)) {
$html = str_replace($this->config->find_string, $this->config->replace_string, $html, $_count);
$this->debug("Strings replaced: $_count (find_string and/or replace_string)");
} else {
$this->debug('Skipped string replacement - incorrect number of find-replace strings in site config');
}
unset($_count);
}
// a lot of code goes after
}
I tried to replace str_replace() with preg_replace(), but while testing it shows an error:
Warning: preg_replace(): No ending matching delimiter '>' found in this line:
$html = preg_replace($this->config->find_string, $this->config->replace_string, $html, $_count);
Where is the error and how to replace str_replace() function to preg_replace() correctly?
I'm very very beginning in php, so any help is badly needed.
Big thanks in advance!
Rewrite your process function like this:
public function process($html, $url, $smart_tidy=true) {
// a lot of code goes before
// do string replacements
if (!empty($this->config->find_string)) {
if (count($this->config->find_string) == count($this->config->replace_string)) {
$new_config_find_string = array_map(function($new_pattern)
{
return '/'.preg_quote($new_pattern).'/';
},$this->config->find_string);
$html = preg_replace($new_config_find_string, $this->config->replace_string, $html, $_count);
$this->debug("Strings replaced: $_count (find_string and/or replace_string)");
} else {
$this->debug('Skipped string replacement - incorrect number of find-replace strings in site config');
}
unset($_count);
}
// a lot of code goes after
}
I have a simple data format that goes as follows:
stuff/stuff/stuff
An example would be:
data/test/hello/hello2
In order to retrieve a certain piece of data, one would use my parser, which tries to do the following:
In data/test/hello/hello2
You want to retrieve the data under data/test (which is hello). My parser's code is below:
function getData($data, $pattern)
{
$info = false;
$dataLineArray = explode("\n", $data);
foreach($dataLineArray as &$line)
{
if (strpos($line,$pattern) !== false) {
$lineArray = explode("/", $line);
$patternArray = explode("/", $pattern);
$iteration = 0;
foreach($lineArray as &$lineData)
{
if($patternArray[$iteration] == $lineData)
{
$iteration++;
}
else
{
$info = $lineData;
}
}
}
}
return $info;
}
However, it always seems to return the last item, which in this case is hello2:
echo getData("data/test/hello/hello2", "data/test");
Gives Me;
hello2
What am I doing wrong?
If you want the first element after the pattern, put break in the loop:
foreach($lineArray as $lineData)
{
if($patternArray[$iteration] == $lineData)
{
$iteration++;
}
elseif ($iteration == count($patternArray))
{
$info = $lineData;
break;
}
}
I also check $iteration == count($patternArray) so that it won't return intermediate elements, e.g.
/data/foo/test/hello/hello2
will return hello rather than foo.
P.S. There doesn't seem to be any reason to use references instead of ordinary variables in your loops, since you never assign to the reference variables.
I have a string which may or may not contain commas. If it does, I want it exploded into an array; if it doesn't, I still want the string saved to the new identifier. My code clearly doesn't work. Anyone have any better ideas?
if(explode(",", $_SESSION['shoparea']))
{
$areas = explode(",", $_SESSION['shoparea']);
} else {
$areas = $_SESSION['shoparea'];
}
What is the correct syntax for this operation?
if(strpos($_SESSION['shoparea'], ',') !== false) {
$areas = explode(',', $_SESSION['shoparea']);
} else {
$areas = $_SESSION['shoparea'];
}
Everything can be exploded, if there are no instances of the delimiter it becomes a singleton array, so it may be simpler to do
$result = explode(",", $_SESSION['shoparea']);
if (count($result) == 1)
$areas = $result[0];
else
$areas = $result;
You could use http://php.net/strpos function to ensure that ',' are present.
you could do this for examle
$areas = $_SESSION['shoparea'];
if(strpos($areas, ',') !== false) {
$areas = explode(",", $areas);
}
All you need is
$_SESSION['shoparea'] = "xx"; // Test value ..
if (!$areas = explode(",", $_SESSION['shoparea'])) {
$areas = array($_SESSION['shoparea']);
}
Output
array
0 => string 'xx' (length=2)
Note : $areas needs to always be array .. if you are using a loop you might have issue so i converted it ..
if (substr_count($_SESSION['shoparea'], ',') > 0) {
$areas = explode(",", $_SESSION['shoparea']);
}
else {
$areas = $_SESSION['shoparea'];
}
I have a script which handles the naming of parent/child elements on another page. The format for the name is like E5-2-3 which represents the third child of the second child of the fifth element.
What I need to do is pass in the parent name to the function and return the name for the next child. This value would be the increment of the last child or 1 if it is the first child.
(I hope this makes some sense to someone)
The index array looks something like this:
1=>null
2=>null
3=>
1=>null
2=>null
3=>
1=>null
4=>null
5=>
1=>null
2=>
1=>null
2=>null
3=>null //the element I was talking about above
6=>
1=>null
7=>null
My Code so far is
$projectNumber = $_GET['project_number'];
#$parentNumber = $_GET['parent_number']; //suppressed as it may not be set
$query = mysql_query("SELECT e_numbers FROM project_management WHERE project_number = '$projectNumber'");
$resultArray = mysql_fetch_assoc($query);
$eNumbers = unserialize($resultArray['e_numbers']);
if (!is_array($eNumbers)&&!isset($parentNumber)){ //first e_number assigned
$eNumbers[1] = null; //cant possibly have children so null for now
$nextENumber = 'E1';
}else{
if (!isset($parentNumber)){
$nextNumber = count($eNumbers)+1;
$eNumbers[$nextNumber] = null; //cant possibly have children so null for now
$nextENumber = 'E'.$nextNumber;
}else{
$parentIndex = explode('-', str_replace('E', '', $parentNumber));
//$nextENumber = //assign $nextENumber the incremented e number
}
}
echo $nextENumber;
//(then goes on to update sql etc etc)
This is all fine but for the line where I need to get/assign deep numbers. I think this should be some kind of recursive function based on the $parentIndex and $eNumbers arrays, however I'm a bit out of my depth when it comes to recursion.
Any pointer in the right direction will be a great help.
PS
If there is a better way to handle incrementing parent/child relationships I'm all ears. The only thing out of my control is the format of the numbers being passed in/out (Has to be EX-Y-Z-...)
UPDATE I was able to develop #ircmaxell 's function to function more better in my context. The function required you to pass in a zero based array(can be empty) and an optional path. It returns the new path and updates the index array to include the new path. An error message is returned if the index is not found.
function getNextPath(&$array, $path) { //thanks to ircmaxell # stackoverflow for the basis of this function
$newPath = '';
$tmp =& $array;
if (is_string($path)) {
$path = explode('-', str_replace('E', '', $path));
$max = count($path);
foreach ($path as $key => $subpath) {
if (is_array($tmp)) {
if (array_key_exists($subpath, $tmp)){
$tmp =& $tmp[$subpath];
$newPath[] = $subpath;
}else{
return "Parent Path Not Found";
}
}
}
}
$tmp[] = null;
$newPath[] = count($tmp)-1;
if (count($newPath)>1){
$newPath = implode('-', $newPath);
}else{
$newPath = $newPath[0];
}
return "E".$newPath;
}
Here's one way:
function incrementPath(&$array, $path) {
if (is_string($path)) {
$path = explode('-', str_replace('E', '', $path);
}
$tmp =& $array;
foreach ($path as $subpath) {
if (is_array($tmp) && isset($tmp[$subpath])) {
$tmp =& $tmp[$subpath];
} else {
return false; // Could not find entire path
}
}
$tmp++;
return true;
}
Now, if you want it to dynamically create paths, just change the return false; to:
$tmp[$subpath] = array();
$tmp =& $tmp[$subpath];
And then add a check after the loop to see if it's not an integer, and explicitly set to 0 if it isn't...
Edit: AHHH, now I understand:
function getNextPath(&$array, $path) {
if (is_string($path)) {
$path = explode('-', str_replace('E', '', $path);
}
$newPath = '';
$tmp =& $array;
$max = count($path) - 1;
foreach ($path as $key => $subpath) {
if (is_array($tmp) && isset($tmp[$subpath])) {
$tmp =& $tmp[$subpath];
if ($key < $max) {
$newPath .= '-'.$subpath;
}
} else {
return 'E' . ltrim($newPath . '-1', '-'); // Could not find entire path
}
}
if (is_array($tmp)) {
return 'E' . ltrim($newPath . '-' . count($tmp), '-');
} else {
//it's a value, so make it an array
$tmp = array();
return 'E' . ltrim($newPath . '-' . 1, '-');
}
}
I think that should do what you want (it returns the next available path under what you're looking for).
}
I am very new to regex, and this is way too advanced for me. So I am asking the experts over here.
Problem
I would like to retrieve the constants / values from a php define()
DEFINE('TEXT', 'VALUE');
Basically I would like a regex to be able to return the name of constant, and the value of constant from the above line. Just TEXT and VALUE . Is this even possible?
Why I need it? I am dealing with language file and I want to get all couples (name, value) and put them in array. I managed to do it with str_replace() and trim() etc.. but this way is long and I am sure it could be made easier with single line of regex.
Note: The VALUE may contain escaped single quotes as well. example:
DEFINE('TEXT', 'J\'ai');
I hope I am not asking for something too complicated. :)
Regards
For any kind of grammar-based parsing, regular expressions are usually an awful solution. Even smple grammars (like arithmetic) have nesting and it's on nesting (in particular) that regular expressions just fall over.
Fortunately PHP provides a far, far better solution for you by giving you access to the same lexical analyzer used by the PHP interpreter via the token_get_all() function. Give it a character stream of PHP code and it'll parse it into tokens ("lexemes"), which you can do a bit of simple parsing on with a pretty simple finite state machine.
Run this program (it's run as test.php so it tries it on itself). The file is deliberately formatted badly so you can see it handles that with ease.
<?
define('CONST1', 'value' );
define (CONST2, 'value2');
define( 'CONST3', time());
define('define', 'define');
define("test", VALUE4);
define('const5', //
'weird declaration'
) ;
define('CONST7', 3.14);
define ( /* comment */ 'foo', 'bar');
$defn = 'blah';
define($defn, 'foo');
define( 'CONST4', define('CONST5', 6));
header('Content-Type: text/plain');
$defines = array();
$state = 0;
$key = '';
$value = '';
$file = file_get_contents('test.php');
$tokens = token_get_all($file);
$token = reset($tokens);
while ($token) {
// dump($state, $token);
if (is_array($token)) {
if ($token[0] == T_WHITESPACE || $token[0] == T_COMMENT || $token[0] == T_DOC_COMMENT) {
// do nothing
} else if ($token[0] == T_STRING && strtolower($token[1]) == 'define') {
$state = 1;
} else if ($state == 2 && is_constant($token[0])) {
$key = $token[1];
$state = 3;
} else if ($state == 4 && is_constant($token[0])) {
$value = $token[1];
$state = 5;
}
} else {
$symbol = trim($token);
if ($symbol == '(' && $state == 1) {
$state = 2;
} else if ($symbol == ',' && $state == 3) {
$state = 4;
} else if ($symbol == ')' && $state == 5) {
$defines[strip($key)] = strip($value);
$state = 0;
}
}
$token = next($tokens);
}
foreach ($defines as $k => $v) {
echo "'$k' => '$v'\n";
}
function is_constant($token) {
return $token == T_CONSTANT_ENCAPSED_STRING || $token == T_STRING ||
$token == T_LNUMBER || $token == T_DNUMBER;
}
function dump($state, $token) {
if (is_array($token)) {
echo "$state: " . token_name($token[0]) . " [$token[1]] on line $token[2]\n";
} else {
echo "$state: Symbol '$token'\n";
}
}
function strip($value) {
return preg_replace('!^([\'"])(.*)\1$!', '$2', $value);
}
?>
Output:
'CONST1' => 'value'
'CONST2' => 'value2'
'CONST3' => 'time'
'define' => 'define'
'test' => 'VALUE4'
'const5' => 'weird declaration'
'CONST7' => '3.14'
'foo' => 'bar'
'CONST5' => '6'
This is basically a finite state machine that looks for the pattern:
function name ('define')
open parenthesis
constant
comma
constant
close parenthesis
in the lexical stream of a PHP source file and treats the two constants as a (name,value) pair. In doing so it handles nested define() statements (as per the results) and ignores whitespace and comments as well as working across multiple lines.
Note: I've deliberatley made it ignore the case when functions and variables are constant names or values but you can extend it to that as you wish.
It's also worth pointing out that PHP is quite forgiving when it comes to strings. They can be declared with single quotes, double quotes or (in certain circumstances) with no quotes at all. This can be (as pointed out by Gumbo) be an ambiguous reference reference to a constant and you have no way of knowing which it is (no guaranteed way anyway), giving you the chocie of:
Ignoring that style of strings (T_STRING);
Seeing if a constant has already been declared with that name and replacing it's value. There's no way you can know what other files have been called though nor can you process any defines that are conditionally created so you can't say with any certainty if anything is definitely a constant or not nor what value it has; or
You can just live with the possibility that these might be constants (which is unlikely) and just treat them as strings.
Personally I would go for (1) then (3).
This is possible, but I would rather use get_defined_constants(). But make sure all your translations have something in common (like all translations starting with T), so you can tell them apart from other constants.
Try this regular expression to find the define calls:
/\bdefine\(\s*("(?:[^"\\]+|\\(?:\\\\)*.)*"|'(?:[^'\\]+|\\(?:\\\\)*.)*')\s*,\s*("(?:[^"\\]+|\\(?:\\\\)*.)*"|'(?:[^'\\]+|\\(?:\\\\)*.)*')\s*\);/is
So:
$pattern = '/\\bdefine\\(\\s*("(?:[^"\\\\]+|\\\\(?:\\\\\\\\)*.)*"|\'(?:[^\'\\\\]+|\\\\(?:\\\\\\\\)*.)*\')\\s*,\\s*("(?:[^"\\\\]+|\\\\(?:\\\\\\\\)*.)*"|\'(?:[^\'\\\\]+|\\\\(?:\\\\\\\\)*.)*\')\\s*\\);/is';
$str = '<?php define(\'foo\', \'bar\'); define("define(\\\'foo\\\', \\\'bar\\\')", "define(\'foo\', \'bar\')"); ?>';
preg_match_all($pattern, $str, $matches, PREG_SET_ORDER);
var_dump($matches);
I know that eval is evil. But that’s the best way to evaluate the string expressions:
$constants = array();
foreach ($matches as $match) {
eval('$constants['.$match[1].'] = '.$match[1].';');
}
var_dump($constants);
You might not need to go overboard with the regex complexity - something like this will probably suffice
/DEFINE\('(.*?)',\s*'(.*)'\);/
Here's a PHP sample showing how you might use it
$lines=file("myconstants.php");
foreach($lines as $line) {
$matches=array();
if (preg_match('/DEFINE\(\'(.*?)\',\s*\'(.*)\'\);/i', $line, $matches)) {
$name=$matches[1];
$value=$matches[2];
echo "$name = $value\n";
}
}
Not every problem with text should be solved with a regexp, so I'd suggest you state what you want to achieve and not how.
So, instead of using php's parser which is not really useful, or instead of using a completely undebuggable regexp, why not write a simple parser?
<?php
$str = "define('nam\\'e', 'va\\\\\\'lue');\ndefine('na\\\\me2', 'value\\'2');\nDEFINE('a', 'b');";
function getDefined($str) {
$lines = array();
preg_match_all('#^define[(][ ]*(.*?)[ ]*[)];$#mi', $str, $lines);
$res = array();
foreach ($lines[1] as $cnt) {
$p = 0;
$key = parseString($cnt, $p);
// Skip comma
$p++;
// Skip space
while ($cnt{$p} == " ") {
$p++;
}
$value = parseString($cnt, $p);
$res[$key] = $value;
}
return $res;
}
function parseString($s, &$p) {
$quotechar = $s[$p];
if (! in_array($quotechar, array("'", '"'))) {
throw new Exception("Invalid quote character '" . $quotechar . "', input is " . var_export($s, true) . " # " . $p);
}
$len = strlen($s);
$quoted = false;
$res = "";
for ($p++;$p < $len;$p++) {
if ($quoted) {
$quoted = false;
$res .= $s{$p};
} else {
if ($s{$p} == "\\") {
$quoted = true;
continue;
}
if ($s{$p} == $quotechar) {
$p++;
return $res;
}
$res .= $s{$p};
}
}
throw new Exception("Premature end of line");
}
var_dump(getDefined($str));
Output:
array(3) {
["nam'e"]=>
string(7) "va\'lue"
["na\me2"]=>
string(7) "value'2"
["a"]=>
string(1) "b"
}