#!/usr/bin/env php * @package ZenTaoATF * @version $Id$ * @link http://www.zentao.net * * Todo: * 1. 帮助支持。 * 2. 指定用例或者目录运行。 * 4. 和禅道的绑定集成。 * 5. 保存日志文件考虑换个目录。windows下面,或者单独创建一个目录。每次执行删除若干分钟之前的? * 6. 输出日志的时候根据操作系统判断换行符。 * 9. 不同的操作系统有不同的skipfile。 */ /* Turn off error reporting. */ error_reporting(E_ALL); /* Short DIRECTORY_SEPARATOR . */ define('DS', DIRECTORY_SEPARATOR); /* Config. */ $config = new stdclass(); $config->timezone = 'Asia/Shanghai'; $config->skipfiles = 'exe,dll,gif,jpg,png,bmp'; $config->casePrefix = 'TC'; $config->casePostfix = 'TC'; /* Ansi control codes. */ $config->ansi = new stdclass(); $config->ansi->off = "\033[0m"; $config->ansi->red = "\033[31m"; $config->ansi->green = "\033[32m"; $config->ansi->yellow = "\033[33m"; $config->ansi->purple = "\033[35m"; /* Supportted languages. */ $config->langs['en']['label'] = 'English'; $config->langs['cn']['label'] = 'Chinese Simplified'; $config->langs['cn']['charsets']['gbk'] = 'GBK'; $config->langs['cn']['charsets']['utf-8'] = 'UTF-8'; /* Languages. */ $lang = new stdclass(); $lang->en = new stdclass(); $lang->cn = new stdclass(); $lang->en->foundScripts = "%s, found %s scripts.\n\n"; $lang->en->failedScripts = "\nFailed scripts:\n"; $lang->en->summary = "\n%s run %s scripts in %s seconds. "; $lang->en->reportFile = "Report: %s\n"; $lang->cn->foundScripts = "%s, 共发现了%s个脚本。\n\n"; $lang->cn->failedScripts = "\n失败的用例:\n"; $lang->cn->summary = "\n%s 共执行了%s个用例,耗时%s秒. "; $lang->cn->reportFile = "报表文件:%s\n"; /* Run testing. */ zentaotest::run(isset($argv[1]) ? $argv[1] : './'); /* The class defination of zentao auto testing framework. */ class zentaotest { /** * The config object. * * @var object * @access public */ public $config; /** * The language object. * * @var int * @access public */ public $lang; /** * The os type. * * @var string * @access public */ public $os; /** * The temp directory. * * @var string * @access public */ public $temp; /** * The home directory. * * @var string * @access public */ public $home; /** * The profile for user personal settings. * * @var string * @access public */ public $profile; /** * The custom settings. * * @var object * @access public */ public $settings; /** * Current working directory. * * @var string * @access public */ public $cwd = ''; /** * Test scripts. * * @var array * @access public */ public $scripts = array(); /** * Script interpreters. * * @var array * @access public */ public $interpreters = array(); /** * The begin time. * * @var int * @access public */ public $begin; /** * The end time. * * @var int * @access public */ public $end; /** * Current script object. * * @var object * @access public */ public $current; /** * The max length of scripts file names. * * @var float * @access public */ public $maxLength = 0; /** * Results. * * @var string * @access public */ public $results = array('pass' => 0, 'fail' => 0, 'skip' => 0); /** * Failed scripts. * * @var array * @access public */ public $failedScripts = array(); /** * The report file. * * @var string * @access public */ public $reportFile = ''; /** * Report info. * * @var string * @access public */ public $reportInfo = ''; /** * The construct function. * * @access public * @return void */ public function __construct() { $this->setConfig(); $this->setOS(); $this->setTemp(); $this->setHome(); $this->setProfile(); $this->loadSettings(); $this->setLang(); $this->setCWD(); $this->setTimeZone(); $this->setReportFile(); } /** * Set config. * * @access public * @return void */ public function setConfig() { global $config; $this->config = $config; } /** * Set current os, please see:http://en.wikipedia.org/wiki/Uname#Table_of_standard_uname_output. * * @access public * @return void */ public function setOS() { if(stripos(PHP_OS, 'linux') !== false) return $this->os = 'linux'; if(stripos(PHP_OS, 'cygwin') !== false) return $this->os = 'linux'; if(stripos(PHP_OS, 'win') !== false) return $this->os = 'win'; if(stripos(PHP_OS, 'bsd') !== false) return $this->os = 'bsd'; if(stripos(PHP_OS, 'darwin') !== false) return $this->os = 'mac'; } /** * Set the temp directory. * * @access public * @return void */ public function setTemp() { if($this->os == 'win') { if(!empty($_SERVER['TMP'])) $this->temp = realpath($_SERVER['TMP']); if(!empty($_SERVER['TEMP'])) $this->temp = realpath($_SERVER['TEMP']); if(empty($this->temp)) $this->temp = dirname(__FILE__) . DS; } else { $this->temp = '/tmp/'; } if(!is_writable($this->temp)) $this->error("The temp directory $this->temp is not writable\n"); } /** * Set the home directory for current user. * * @access public * @return void */ public function setHome() { if($this->os == 'win') { if(isset($_SERVER['HOMEDRIVE']) and isset($_SERVER['HOMEPATH'])) { $this->home = $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH'] . DS; } else { $this->home = $this->temp; } } else { $this->home = getenv('HOME') . DS; } } /** * Set the profile. * * @access public * @return void */ public function setProfile() { $this->profile = $this->home . '.ztconfig'; if(!is_file($this->profile)) touch($this->profile); } /** * Load settings from the profile. * * @access public * @return void */ public function loadSettings() { $this->settings = json_decode(file_get_contents($this->profile)); if(!$this->settings) { $this->settings = new stdclass(); $this->settings->interpreters = new stdclass(); } } /** * Save settings to profile. * * @access public * @return void */ public function saveSettings() { file_put_contents($this->profile, json_encode($this->settings)); } /** * Set language. * * @access public * @return void */ public function setLang() { if(!isset($this->settings->lang)) { while(true) { $langs = array_keys($this->config->langs); echo "Please select your favorite language:\n"; $i = 1; foreach($this->config->langs as $lang) { echo "($i) $lang[label]\n"; $i ++; } $input = trim(fgets(STDIN)); $langCode = isset($langs[$input - 1]) ? $langs[$input - 1] : ''; if(!$langCode) continue; $lang = $this->config->langs[$langCode]; $charset = 'utf-8'; if(isset($lang['charsets'])) { $charsets = array_keys($lang['charsets']); while(true) { echo "Please select charset:\n"; $i = 1; foreach($lang['charsets'] as $charset) { echo "($i) $charset\n"; $i ++; } $input = trim(fgets(STDIN)); $charset = isset($charsets[$input - 1]) ? $charsets[$input - 1] : ''; if(!$charset) continue; break; } } /* Update the settings. */ $this->settings->lang = $langCode; $this->settings->charset = $charset; $this->saveSettings(); break; } } global $lang; $this->lang = $lang->{$this->settings->lang}; } /** * Set current working directory. * * @access public * @return void */ public function setCWD() { $this->cwd = getcwd() . DS; } /** * Set the time zone. * * @access public * @return void */ public function setTimeZone() { date_default_timezone_set($this->config->timezone); } /** * Set the report file. * * @access public * @return void */ public function setReportFile() { $this->reportFile = $this->temp . date('Ymd.Hi') . '.log'; } /** * Run testing scripts. * * @param string $dir * @access public * @return void */ public static function run($dir) { $zt = new zentaotest(); $zt->logBegin(); $zt->findScripts($dir); $zt->setInterpreters(); $zt->setMaxLength($zt->scripts); $zt->runScripts(); $zt->logEnd(); $zt->printFailed(); $zt->printSummary(); $zt->saveReport(); } /** * Log the begin time. * * @access public * @return void */ public function logBegin() { $this->begin = microtime(true); } /** * Find testing scripts under a directory. * * @param string $dir * @access public * @return void */ public function findScripts($dir = '.') { if(is_file($dir)) return $this->scripts[] = realpath($dir); $scripts = glob($dir . '/' . '*'); foreach($scripts as $script) { if(basename($script) == 'zt') continue; if(is_dir($script)) { $this->findScripts($script); } else { /* Get the script interpreter. */ $interpreter = strtolower(pathinfo($script, PATHINFO_EXTENSION)); if($interpreter and strpos($this->config->skipfiles, $interpreter) !== false) continue; /* Has TC_BEGIN in the script, save it. */ if(strpos(file_get_contents($script), $this->config->casePrefix) !== false) { $this->scripts[] = realpath($script); if($interpreter) $this->interpreters[$interpreter] = $interpreter; } } } } /** * Set interpreters for scripts. * * @access public * @return void */ public function setInterpreters() { if($this->os != 'win') return; foreach($this->interpreters as $interpreter) { if(isset($this->settings->interpreters->$interpreter)) continue; if($interpreter == 'bat') continue; while(true) { echo "Please set interpreter for $interpreter scripts, enter to skip:"; $input = trim(fgets(STDIN)); if(!is_file($input)) { if(empty($input)) break; echo "The interpreter you input not exists, please try again.\n"; continue; } $this->settings->interpreters->$interpreter = $input; break; } } $this->saveSettings(); } /** * Set the max length of scripts filenames. * * @access public * @param array $scripts * @access public * @return void */ public function setMaxLength($scripts) { $this->maxLength = 0; foreach($scripts as $script) { $script = str_replace($this->cwd, '', $script); $length = strlen($script); if($length > $this->maxLength) $this->maxLength = $length; } } /** * Batch run testing scripts. * * @access public * @return void */ public function runScripts() { $this->printTotal(); foreach($this->scripts as $key => $script) { $this->printNumber($key); $this->runScript($script); $this->printResult(); $this->printFileName(); $this->printTitle(); $this->printTime(); } } /** * Print the total info. * * @access public * @return void */ public function printTotal() { $totalInfo = sprintf($this->processLang($this->lang->foundScripts), date('n/j G:i:s'), $this->colorString(count($this->scripts), $this->config->ansi->yellow)); (print($totalInfo)) && $this->reportInfo .= $totalInfo; } /** * Print the number of current script. * * @param int $key * @access public * @return void */ public function printNumber($key) { $total = count($this->scripts); $padLen = strlen($total); $key = str_pad($key + 1, $padLen, ' ', STR_PAD_LEFT); $keyInfo = "($key/$total) "; (print($keyInfo)) && $this->reportInfo .= $keyInfo; } /** * Run a script to get it's result. * * @param string $script * @access public * @return void */ public function runScript($script) { $begin = microtime(true); $this->parseScript($script); $this->getScriptOutput(); $this->computeResult(); $this->current->time = microtime(true) - $begin; } /** * Parse the script to extract the case definations. * * @param string $script * @access public * @return void */ public function parseScript($script) { /* Extract the yaml defination. */ $contents = trim(file_get_contents($script)); $begin = strpos($contents, $this->config->casePrefix) + strlen($this->config->casePrefix); $end = strpos($contents, $this->config->casePostfix); $yaml = substr($contents, $begin, $end - $begin); /* Parse the yaml. */ $this->current = (object)spyc::yamlLoadString($yaml); /* Set the default value of expect. */ if(!isset($this->current->expect)) $this->current->expect = ''; /* Append the script file name and interpreter. */ $this->current->script = $script; $this->current->interpreter = pathinfo($script, PATHINFO_EXTENSION); /* Check whether the expect file exists. */ $path = pathinfo($script); $expectFile = str_replace($path['basename'], '.' . $path['basename'], $script); $expectFile = str_replace('.'. $path['extension'], '.et', $expectFile); if(is_file($expectFile)) return $this->current->expect = trim($this->dos2unix(file_get_contents($expectFile))); /* Check whether the expectx file exists. */ $expectxFile = str_replace('.et', '.ex', $expectFile); if(is_file($expectxFile)) return $this->current->expectx = trim($this->dos2unix(file_get_contents($expectxFile))); } /** * System the script file to get it's real output. * * @access public * @return void */ public function getScriptOutput() { chdir(dirname($this->current->script)); if($this->os == 'win') { $interpreter = $this->current->interpreter; if($interpreter == 'bat') { $this->current->output = trim(`{$this->current->script}`); } elseif(isset($this->settings->interpreters->$interpreter)) { $this->current->output = trim(`{$this->settings->interpreters->$interpreter} {$this->current->script}`); } else { $this->current->output = "skip"; } } else { $this->current->output = trim(`{$this->current->script}`); } chdir($this->cwd); } /** * Compute the result by comparing the expect and output. * * @access public * @return void */ public function computeResult() { /* If the output is skip, skip this script. */ if($this->current->output == 'skip') { $this->current->result = 'skip'; $this->results['skip'] ++; return true; } /* First try to compare the expect and output. */ if(strcmp($this->current->expect, $this->current->output) === 0) { $this->current->result = 'pass'; $this->results['pass'] ++; return true; } /* Then try to get regular from expectx or try convert format chars in expect field.. */ $regulars = ''; if(isset($this->current->expectx)) { $regulars = $this->current->expectx; } elseif(strpos($this->current->expect, '%') !== false) { $expect = $this->convertFormat2REG($this->current->expect); if($expect != $this->current->expect) $regulars = $expect; } /* No regulars found, fail. */ if(!$regulars) { $this->current->result = 'fail'; $this->current->diff = $this->diff($this->current->expect, $this->current->output) . "\n"; $this->failedScripts[] = $this->current; $this->results['fail'] ++; return false; } /* Compare the results by regular. */ $regulars = explode("\n", $regulars); $output = explode("\n", $this->current->output); foreach($regulars as $line => $regular) { if(isset($output[$line]) and preg_match("/^$regular$/s", $output[$line])) $regulars[$line] = $output[$line]; } if($regulars == $output) { $this->current->result = 'pass'; $this->results['pass'] ++; return true; } else { if(isset($this->current->expectx)) $diff = $this->diff(join("\n", $regulars), join("\n", $output)); if(!isset($this->current->expectx)) $diff = $this->diff($this->current->expect, $this->current->output); $this->current->result = 'fail'; $this->current->diff = $diff; $this->failedScripts[] = $this->current; $this->results['fail'] ++; return false; } } /** * Convert format string to reg. * * This function is copied from run-test.php of php project. * Please see: http://qa.php.net/phpt_details.php#expectf_section. * * @param string $string * @access public * @return string */ public function convertFormat2REG($string) { $string = preg_replace('/\r\n/', "\n", $string); $string = preg_quote($string, '/'); $string = str_replace('%e', '\\' . DS, $string); $string = str_replace('%s', '[^\r\n]+', $string); $string = str_replace('%S', '[^\r\n]*', $string); $string = str_replace('%a', '.+', $string); $string = str_replace('%A', '.*', $string); $string = str_replace('%w', '\s*', $string); $string = str_replace('%i', '[+-]?\d+', $string); $string = str_replace('%d', '\d+', $string); $string = str_replace('%x', '[0-9a-fA-F]+', $string); $string = str_replace('%f', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', $string); $string = str_replace('%c', '.', $string); return $string; } /** * Diff two string, copy from the phpt(http://qa.php.net/) * * @param string $wanted * @param string $out * @access public * @return string */ public function diff($expect, $out) { $expect = str_replace("\r", '', $expect); $out = str_replace("\r", '', $out); $w = explode("\n", $expect); $o = explode("\n", $out); $w1 = array_diff_assoc($w,$o); $o1 = array_diff_assoc($o,$w); $w2 = array(); $o2 = array(); foreach($w1 as $idx => $val) $w2[sprintf("%03d<",$idx)] = sprintf("%03d- ", $idx+1).$val; foreach($o1 as $idx => $val) $o2[sprintf("%03d>",$idx)] = sprintf("%03d+ ", $idx+1).$val; $diff = array_merge($w2, $o2); ksort($diff); return implode("\n", $diff); } /** * Print the run result. * * @access public * @return void */ public function printResult() { if($this->current->result == 'pass') $resultInfo = $this->colorString('PASS ', $this->config->ansi->green); if($this->current->result == 'fail') $resultInfo = $this->colorString('FAIL ', $this->config->ansi->red); if($this->current->result == 'skip') $resultInfo = $this->colorString('SKIP ', $this->config->ansi->purple); (print($resultInfo)) && $this->reportInfo .= $resultInfo; } /** * Print file name of script. * * @access public * @return void */ public function printFileName() { $fileName = str_replace($this->cwd, '', $this->current->script); $fileName = '[' . str_pad($fileName, $this->maxLength, ' ') . '] '; (print($fileName)) && $this->reportInfo .= $fileName; } /** * Print title of script. * * @access public * @return void */ public function printTitle() { (print($this->current->title)) && $this->reportInfo .= $this->current->title; } /** * Print time comsumed. * * @access public * @return void */ public function printTime() { $timeInfo = ' (' . round($this->current->time, 2) . 's)' . "\n"; (print($timeInfo)) && $this->reportInfo .= $timeInfo; } /** * Log the end time. * * @access public * @return void */ public function logEnd() { $this->end = microtime(true); } /** * Print failed scripts info. * * @access public * @return void */ public function printFailed() { if(count($this->failedScripts) > 0) { /* Compute the max file name length of failed scripts. */ foreach($this->failedScripts as $script) $scriptFiles[] = $script->script; $this->setMaxLength($scriptFiles); /* Print info of failed scripts. */ $failedInfo = $this->processLang($this->lang->failedScripts); (print($failedInfo)) && $this->reportInfo .= $failedInfo; foreach($this->failedScripts as $script) { $this->current = $script; $this->printFileName(); $this->printTitle(); (print("\n")) && $this->reportInfo .= "\n"; } } } /** * Print summary of this run. * * @access public * @return void */ public function printSummary() { $now = date("n/j G:i:s"); $scripts = $this->colorString(count($this->scripts), $this->config->ansi->yellow); $seconds = round($this->end - $this->begin, 2); $summary = sprintf($this->processLang($this->lang->summary), $now, $scripts, $seconds); $passRate = '(' . round($this->results['pass'] / count($this->scripts), 3) * 100 . '%' . ')'; $failRate = '(' . round($this->results['fail'] / count($this->scripts), 3) * 100 . '%' . ')'; $skipRate = '(' . round($this->results['skip'] / count($this->scripts), 3) * 100 . '%' . ')'; $summary .= $this->colorString($this->results['pass'] . $passRate . ' pass, ', $this->config->ansi->green); $summary .= $this->colorstring($this->results['fail'] . $failRate . ' fail, ', $this->config->ansi->red); $summary .= $this->colorstring($this->results['skip'] . $skipRate . ' skip. ', $this->config->ansi->purple); (print($summary)) && $this->reportInfo .= $summary; } /** * Save run report to file. * * @access public * @return void */ public function saveReport() { /* Append the diff info for failed scripts. */ if(count($this->failedScripts) > 0) { $this->reportInfo .= "\n"; foreach($this->failedScripts as $script) { $expect = isset($script->expectx) ? $script->expectx : $script->expect; $this->reportInfo .= "\n{{===\nfailed $script->script <$script->title>}}\n"; $this->reportInfo .= "[expect]\n$expect\n\n"; $this->reportInfo .= "[output]\n$script->output\n\n"; $this->reportInfo .= "[diff]\n$script->diff"; $this->reportInfo .= "\n===}}\n"; } } file_put_contents($this->reportFile, $this->removeColor($this->reportInfo)); printf($this->processLang($this->lang->reportFile), $this->colorString($this->reportFile, $this->config->ansi->yellow)); } /** * Print a string with color if the os is not windows. * * @param string $string * @param string $color * @access public * @return string */ public function colorString($string, $color) { if($this->os == 'win') return $string; return $color . $string . $this->config->ansi->off; } /** * Remove the color marking chars. * * @param string $string * @access public * @return string */ public function removeColor($string) { $string = str_replace($this->config->ansi->green, '', $string); $string = str_replace($this->config->ansi->red, '', $string); $string = str_replace($this->config->ansi->yellow, '', $string); $string = str_replace($this->config->ansi->purple, '', $string); $string = str_replace($this->config->ansi->off, '', $string); return $string; } /** * Convert a string fro dos format to unix. * * @param string $string * @access public * @return string */ public function dos2unix($string) { return str_replace("\r", '', $string); } /** * Process lang item, if the charset is not utf-8, convert it. * * @param string $string * @access public * @return string */ public function processLang($string) { if(!function_exists('iconv') or $this->settings->charset == 'utf-8') return $string; return iconv('utf-8', $this->settings->charset, $string); } /** * Print error message. * * @param string $message * @access public * @return void */ public function error($message) { die("ERROR: " . $message . "\n"); } } //---------------------------------- The spyc yaml parser class ----------------------------------// /** * Spyc -- A Simple PHP YAML Class * @version 0.5 * @author Vlad Andersen * @author Chris Wanstrath * @link http://code.google.com/p/spyc/ * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Spyc */ if (!function_exists('spyc_load')) { /** * Parses YAML to array. * @param string $string YAML string. * @return array */ function spyc_load ($string) { return Spyc::YAMLLoadString($string); } } if (!function_exists('spyc_load_file')) { /** * Parses YAML to array. * @param string $file Path to YAML file. * @return array */ function spyc_load_file ($file) { return Spyc::YAMLLoad($file); } } /** * The Simple PHP YAML Class. * * This class can be used to read a YAML file and convert its contents * into a PHP array. It currently supports a very limited subsection of * the YAML spec. * * Usage: * * $Spyc = new Spyc; * $array = $Spyc->load($file); * * or: * * $array = Spyc::YAMLLoad($file); * * or: * * $array = spyc_load_file($file); * * @package Spyc */ class Spyc { // SETTINGS const REMPTY = "\0\0\0\0\0"; /** * Setting this to true will force YAMLDump to enclose any string value in * quotes. False by default. * * @var bool */ public $setting_dump_force_quotes = false; /** * Setting this to true will forse YAMLLoad to use syck_load function when * possible. False by default. * @var bool */ public $setting_use_syck_is_possible = false; /**#@+ * @access private * @var mixed */ private $_dumpIndent; private $_dumpWordWrap; private $_containsGroupAnchor = false; private $_containsGroupAlias = false; private $path; private $result; private $LiteralPlaceHolder = '___YAML_Literal_Block___'; private $SavedGroups = array(); private $indent; /** * Path modifier that should be applied after adding current element. * @var array */ private $delayedPath = array(); /**#@+ * @access public * @var mixed */ public $_nodeId; /** * Load a valid YAML string to Spyc. * @param string $input * @return array */ public function load ($input) { return $this->__loadString($input); } /** * Load a valid YAML file to Spyc. * @param string $file * @return array */ public function loadFile ($file) { return $this->__load($file); } /** * Load YAML into a PHP array statically * * The load method, when supplied with a YAML stream (string or file), * will do its best to convert YAML in a file into a PHP array. Pretty * simple. * Usage: * * $array = Spyc::YAMLLoad('lucky.yaml'); * print_r($array); * * @access public * @return array * @param string $input Path of YAML file or string containing YAML */ public static function YAMLLoad($input) { $Spyc = new Spyc; return $Spyc->__load($input); } /** * Load a string of YAML into a PHP array statically * * The load method, when supplied with a YAML string, will do its best * to convert YAML in a string into a PHP array. Pretty simple. * * Note: use this function if you don't want files from the file system * loaded and processed as YAML. This is of interest to people concerned * about security whose input is from a string. * * Usage: * * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); * print_r($array); * * @access public * @return array * @param string $input String containing YAML */ public static function YAMLLoadString($input) { $Spyc = new Spyc; return $Spyc->__loadString($input); } /** * Dump YAML from PHP array statically * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as nothing.yaml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @access public * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ public static function YAMLDump($array,$indent = false,$wordwrap = false) { $spyc = new Spyc; return $spyc->dump($array,$indent,$wordwrap); } /** * Dump PHP array to YAML * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as tasteful.yaml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @access public * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ public function dump($array,$indent = false,$wordwrap = false) { // Dumps to some very clean YAML. We'll have to add some more features // and options soon. And better support for folding. // New features and options. if ($indent === false or !is_numeric($indent)) { $this->_dumpIndent = 2; } else { $this->_dumpIndent = $indent; } if ($wordwrap === false or !is_numeric($wordwrap)) { $this->_dumpWordWrap = 40; } else { $this->_dumpWordWrap = $wordwrap; } // New YAML document $string = "---\n"; // Start at the base of the array and move through it. if ($array) { $array = (array)$array; $previous_key = -1; foreach ($array as $key => $value) { if (!isset($first_key)) $first_key = $key; $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key, $array); $previous_key = $key; } } return $string; } /** * Attempts to convert a key / value array item to YAML * @access private * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ private function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0, $source_array = null) { if (is_array($value)) { if (empty ($value)) return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array); // It has children. What to do? // Make it the right kind of item $string = $this->_dumpNode($key, self::REMPTY, $indent, $previous_key, $first_key, $source_array); // Add the indent $indent += $this->_dumpIndent; // Yamlize the array $string .= $this->_yamlizeArray($value,$indent); } elseif (!is_array($value)) { // It doesn't have children. Yip. $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array); } return $string; } /** * Attempts to convert an array to YAML * @access private * @return string * @param $array The array you want to convert * @param $indent The indent of the current level */ private function _yamlizeArray($array,$indent) { if (is_array($array)) { $string = ''; $previous_key = -1; foreach ($array as $key => $value) { if (!isset($first_key)) $first_key = $key; $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array); $previous_key = $key; } return $string; } else { return false; } } /** * Returns YAML from a key and a value * @access private * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ private function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { // do some folding here, for blocks if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos ($value, ' ') !== false || strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || strpos($value,"&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 || substr ($value, -1, 1) == ':') ) { $value = $this->_doLiteralBlock($value,$indent); } else { $value = $this->_doFolding($value,$indent); } if ($value === array()) $value = '[ ]'; if (in_array ($value, array ('true', 'TRUE', 'false', 'FALSE', 'y', 'Y', 'n', 'N', 'null', 'NULL'), true)) { $value = $this->_doLiteralBlock($value,$indent); } if (trim ($value) != $value) $value = $this->_doLiteralBlock($value,$indent); if (is_bool($value)) { $value = ($value) ? "true" : "false"; } if ($value === null) $value = 'null'; if ($value === "'" . self::REMPTY . "'") $value = null; $spaces = str_repeat(' ',$indent); //if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { if (is_array ($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) { // It's a sequence $string = $spaces.'- '.$value."\n"; } else { // if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); // It's mapped if (strpos($key, ":") !== false || strpos($key, "#") !== false) { $key = '"' . $key . '"'; } $string = rtrim ($spaces.$key.': '.$value)."\n"; } return $string; } /** * Creates a literal block for dumping * @access private * @return string * @param $value * @param $indent int The value of the indent */ private function _doLiteralBlock($value,$indent) { if ($value === "\n") return '\n'; if (strpos($value, "\n") === false && strpos($value, "'") === false) { return sprintf ("'%s'", $value); } if (strpos($value, "\n") === false && strpos($value, '"') === false) { return sprintf ('"%s"', $value); } $exploded = explode("\n",$value); $newValue = '|'; $indent += $this->_dumpIndent; $spaces = str_repeat(' ',$indent); foreach ($exploded as $line) { $newValue .= "\n" . $spaces . ($line); } return $newValue; } /** * Folds a string of text, if necessary * @access private * @return string * @param $value The string you wish to fold */ private function _doFolding($value,$indent) { // Don't do anything if wordwrap is set to 0 if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { $indent += $this->_dumpIndent; $indent = str_repeat(' ',$indent); $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); $value = ">\n".$indent.$wrapped; } else { if ($this->setting_dump_force_quotes && is_string ($value) && $value !== self::REMPTY) $value = '"' . $value . '"'; } return $value; } // LOADING FUNCTIONS private function __load($input) { $Source = $this->loadFromSource($input); return $this->loadWithSource($Source); } private function __loadString($input) { $Source = $this->loadFromString($input); return $this->loadWithSource($Source); } private function loadWithSource($Source) { if (empty ($Source)) return array(); if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { $array = syck_load (implode ('', $Source)); return is_array($array) ? $array : array(); } $this->path = array(); $this->result = array(); $cnt = count($Source); for ($i = 0; $i < $cnt; $i++) { $line = $Source[$i]; $this->indent = strlen($line) - strlen(ltrim($line)); $tempPath = $this->getParentPathByIndent($this->indent); $line = self::stripIndent($line, $this->indent); if (self::isComment($line)) continue; if (self::isEmpty($line)) continue; $this->path = $tempPath; $literalBlockStyle = self::startsLiteralBlock($line); if ($literalBlockStyle) { $line = rtrim ($line, $literalBlockStyle . " \n"); $literalBlock = ''; $line .= $this->LiteralPlaceHolder; $literal_block_indent = strlen($Source[$i+1]) - strlen(ltrim($Source[$i+1])); while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent); } $i--; } while (++$i < $cnt && self::greedilyNeedNextLine($line)) { $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); } $i--; if (strpos ($line, '#')) { if (strpos ($line, '"') === false && strpos ($line, "'") === false) $line = preg_replace('/\s+#(.+)$/','',$line); } $lineArray = $this->_parseLine($line); if ($literalBlockStyle) $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); $this->addArray($lineArray, $this->indent); foreach ($this->delayedPath as $indent => $delayedPath) $this->path[$indent] = $delayedPath; $this->delayedPath = array(); } return $this->result; } private function loadFromSource ($input) { if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) return file($input); return $this->loadFromString($input); } private function loadFromString ($input) { $lines = explode("\n",$input); foreach ($lines as $k => $_) { $lines[$k] = rtrim ($_, "\r"); } return $lines; } /** * Parses YAML code and returns an array for a node * @access private * @return array * @param string $line A line from the YAML file */ private function _parseLine($line) { if (!$line) return array(); $line = trim($line); if (!$line) return array(); $array = array(); $group = $this->nodeContainsGroup($line); if ($group) { $this->addGroup($line, $group); $line = $this->stripGroup ($line, $group); } if ($this->startsMappedSequence($line)) return $this->returnMappedSequence($line); if ($this->startsMappedValue($line)) return $this->returnMappedValue($line); if ($this->isArrayElement($line)) return $this->returnArrayElement($line); if ($this->isPlainArray($line)) return $this->returnPlainArray($line); return $this->returnKeyValuePair($line); } /** * Finds the type of the passed value, returns the value as the new type. * @access private * @param string $value * @return mixed */ private function _toType($value) { if ($value === '') return null; $first_character = $value[0]; $last_character = substr($value, -1, 1); $is_quoted = false; do { if (!$value) break; if ($first_character != '"' && $first_character != "'") break; if ($last_character != '"' && $last_character != "'") break; $is_quoted = true; } while (0); if ($is_quoted) return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\'\'' => '\'', '\\\'' => '\'')); if (strpos($value, ' #') !== false && !$is_quoted) $value = preg_replace('/\s+#(.+)$/','',$value); if (!$is_quoted) $value = str_replace('\n', "\n", $value); if ($first_character == '[' && $last_character == ']') { // Take out strings sequences and mappings $innerValue = trim(substr ($value, 1, -1)); if ($innerValue === '') return array(); $explode = $this->_inlineEscape($innerValue); // Propagate value array $value = array(); foreach ($explode as $v) { $value[] = $this->_toType($v); } return $value; } if (strpos($value,': ')!==false && $first_character != '{') { $array = explode(': ',$value); $key = trim($array[0]); array_shift($array); $value = trim(implode(': ',$array)); $value = $this->_toType($value); return array($key => $value); } if ($first_character == '{' && $last_character == '}') { $innerValue = trim(substr ($value, 1, -1)); if ($innerValue === '') return array(); // Inline Mapping // Take out strings sequences and mappings $explode = $this->_inlineEscape($innerValue); // Propagate value array $array = array(); foreach ($explode as $v) { $SubArr = $this->_toType($v); if (empty($SubArr)) continue; if (is_array ($SubArr)) { $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; } $array[] = $SubArr; } return $array; } if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { return null; } if ( is_numeric($value) && preg_match ('/^(-|)[1-9]+[0-9]*$/', $value) ){ $intvalue = (int)$value; if ($intvalue != PHP_INT_MAX) $value = $intvalue; return $value; } if (in_array($value, array('true', 'on', '+', 'yes', 'y', 'True', 'TRUE', 'On', 'ON', 'YES', 'Yes', 'Y'))) { return true; } if (in_array(strtolower($value), array('false', 'off', '-', 'no', 'n'))) { return false; } if (is_numeric($value)) { if ($value === '0') return 0; if (rtrim ($value, 0) === $value) $value = (float)$value; return $value; } return $value; } /** * Used in inlines to check for more inlines or quoted strings * @access private * @return array */ private function _inlineEscape($inline) { // There's gotta be a cleaner way to do this... // While pure sequences seem to be nesting just fine, // pure mappings and mappings with sequences inside can't go very // deep. This needs to be fixed. $seqs = array(); $maps = array(); $saved_strings = array(); // Check for strings $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; if (preg_match_all($regex,$inline,$strings)) { $saved_strings = $strings[0]; $inline = preg_replace($regex,'YAMLString',$inline); } unset($regex); $i = 0; do { // Check for sequences while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { $seqs[] = $matchseqs[0]; $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); } // Check for mappings while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { $maps[] = $matchmaps[0]; $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); } if ($i++ >= 10) break; } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); $explode = explode(', ',$inline); $stringi = 0; $i = 0; while (1) { // Re-add the sequences if (!empty($seqs)) { foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { foreach ($seqs as $seqk => $seq) { $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); $value = $explode[$key]; } } } } // Re-add the mappings if (!empty($maps)) { foreach ($explode as $key => $value) { if (strpos($value,'YAMLMap') !== false) { foreach ($maps as $mapk => $map) { $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); $value = $explode[$key]; } } } } // Re-add the strings if (!empty($saved_strings)) { foreach ($explode as $key => $value) { while (strpos($value,'YAMLString') !== false) { $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); unset($saved_strings[$stringi]); ++$stringi; $value = $explode[$key]; } } } $finished = true; foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { $finished = false; break; } if (strpos($value,'YAMLMap') !== false) { $finished = false; break; } if (strpos($value,'YAMLString') !== false) { $finished = false; break; } } if ($finished) break; $i++; if ($i > 10) break; // Prevent infinite loops. } return $explode; } private function literalBlockContinues ($line, $lineIndent) { if (!trim($line)) return true; if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; return false; } private function referenceContentsByAlias ($alias) { do { if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } $groupPath = $this->SavedGroups[$alias]; $value = $this->result; foreach ($groupPath as $k) { $value = $value[$k]; } } while (false); return $value; } private function addArrayInline ($array, $indent) { $CommonGroupPath = $this->path; if (empty ($array)) return false; foreach ($array as $k => $_) { $this->addArray(array($k => $_), $indent); $this->path = $CommonGroupPath; } return true; } private function addArray ($incoming_data, $incoming_indent) { // print_r ($incoming_data); if (count ($incoming_data) > 1) return $this->addArrayInline ($incoming_data, $incoming_indent); $key = key ($incoming_data); $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; if ($key === '__!YAMLZero') $key = '0'; if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. if ($key || $key === '' || $key === '0') { $this->result[$key] = $value; } else { $this->result[] = $value; end ($this->result); $key = key ($this->result); } $this->path[$incoming_indent] = $key; return; } $history = array(); // Unfolding inner array tree. $history[] = $_arr = $this->result; foreach ($this->path as $k) { $history[] = $_arr = $_arr[$k]; } if ($this->_containsGroupAlias) { $value = $this->referenceContentsByAlias($this->_containsGroupAlias); $this->_containsGroupAlias = false; } // Adding string or numeric key to the innermost level or $this->arr. if (is_string($key) && $key == '<<') { if (!is_array ($_arr)) { $_arr = array (); } $_arr = array_merge ($_arr, $value); } else if ($key || $key === '' || $key === '0') { if (!is_array ($_arr)) $_arr = array ($key=>$value); else $_arr[$key] = $value; } else { if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } } $reverse_path = array_reverse($this->path); $reverse_history = array_reverse ($history); $reverse_history[0] = $_arr; $cnt = count($reverse_history) - 1; for ($i = 0; $i < $cnt; $i++) { $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; } $this->result = $reverse_history[$cnt]; $this->path[$incoming_indent] = $key; if ($this->_containsGroupAnchor) { $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; if (is_array ($value)) { $k = key ($value); if (!is_int ($k)) { $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; } } $this->_containsGroupAnchor = false; } } private static function startsLiteralBlock ($line) { $lastChar = substr (trim($line), -1); if ($lastChar != '>' && $lastChar != '|') return false; if ($lastChar == '|') return $lastChar; // HTML tags should not be counted as literal blocks. if (preg_match ('#<.*?>$#', $line)) return false; return $lastChar; } private static function greedilyNeedNextLine($line) { $line = trim ($line); if (!strlen($line)) return false; if (substr ($line, -1, 1) == ']') return false; if ($line[0] == '[') return true; if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; return false; } private function addLiteralLine ($literalBlock, $line, $literalBlockStyle, $indent = -1) { $line = self::stripIndent($line, $indent); if ($literalBlockStyle !== '|') { $line = self::stripIndent($line); } $line = rtrim ($line, "\r\n\t ") . "\n"; if ($literalBlockStyle == '|') { return $literalBlock . $line; } if (strlen($line) == 0) return rtrim($literalBlock, ' ') . "\n"; if ($line == "\n" && $literalBlockStyle == '>') { return rtrim ($literalBlock, " \t") . "\n"; } if ($line != "\n") $line = trim ($line, "\r\n ") . " "; return $literalBlock . $line; } function revertLiteralPlaceHolder ($lineArray, $literalBlock) { foreach ($lineArray as $k => $_) { if (is_array($_)) $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) $lineArray[$k] = rtrim ($literalBlock, " \r\n"); } return $lineArray; } private static function stripIndent ($line, $indent = -1) { if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); return substr ($line, $indent); } private function getParentPathByIndent ($indent) { if ($indent == 0) return array(); $linePath = $this->path; do { end($linePath); $lastIndentInParentPath = key($linePath); if ($indent <= $lastIndentInParentPath) array_pop ($linePath); } while ($indent <= $lastIndentInParentPath); return $linePath; } private function clearBiggerPathValues ($indent) { if ($indent == 0) $this->path = array(); if (empty ($this->path)) return true; foreach ($this->path as $k => $_) { if ($k > $indent) unset ($this->path[$k]); } return true; } private static function isComment ($line) { if (!$line) return false; if ($line[0] == '#') return true; if (trim($line, " \r\n\t") == '---') return true; return false; } private static function isEmpty ($line) { return (trim ($line) === ''); } private function isArrayElement ($line) { if (!$line) return false; if ($line[0] != '-') return false; if (strlen ($line) > 3) if (substr($line,0,3) == '---') return false; return true; } private function isHashElement ($line) { return strpos($line, ':'); } private function isLiteral ($line) { if ($this->isArrayElement($line)) return false; if ($this->isHashElement($line)) return false; return true; } private static function unquote ($value) { if (!$value) return $value; if (!is_string($value)) return $value; if ($value[0] == '\'') return trim ($value, '\''); if ($value[0] == '"') return trim ($value, '"'); return $value; } private function startsMappedSequence ($line) { return ($line[0] == '-' && substr ($line, -1, 1) == ':'); } private function returnMappedSequence ($line) { $array = array(); $key = self::unquote(trim(substr($line,1,-1))); $array[$key] = array(); $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); return array($array); } private function returnMappedValue ($line) { $array = array(); $key = self::unquote (trim(substr($line,0,-1))); $array[$key] = ''; return $array; } private function startsMappedValue ($line) { return (substr ($line, -1, 1) == ':'); } private function isPlainArray ($line) { return ($line[0] == '[' && substr ($line, -1, 1) == ']'); } private function returnPlainArray ($line) { return $this->_toType($line); } private function returnKeyValuePair ($line) { $array = array(); $key = ''; if (strpos ($line, ':')) { // It's a key/value pair most likely // If the key is in double quotes pull it out if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { $value = trim(str_replace($matches[1],'',$line)); $key = $matches[2]; } else { // Do some guesswork as to the key and the value $explode = explode(':',$line); $key = trim($explode[0]); array_shift($explode); $value = trim(implode(':',$explode)); } // Set the type of the value. Int, string, etc $value = $this->_toType($value); if ($key === '0') $key = '__!YAMLZero'; $array[$key] = $value; } else { $array = array ($line); } return $array; } private function returnArrayElement ($line) { if (strlen($line) <= 1) return array(array()); // Weird %) $array = array(); $value = trim(substr($line,1)); $value = $this->_toType($value); $array[] = $value; return $array; } private function nodeContainsGroup ($line) { $symbolsForReference = 'A-z0-9_\-'; if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; return false; } private function addGroup ($line, $group) { if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); //print_r ($this->path); } private function stripGroup ($line, $group) { $line = trim(str_replace($group, '', $line)); return $line; } }