1, 'preg_match' => 1, 'preg_split' => 1, 'it__match' => 1, 'it__replace' => 1, 'it__filter' => 1, 'it__split' => 1); var $lines; # all lines currently in pipe var $_valid; var $_position; /** * Creates a pipe object from input. Named arguments: * $p['fn'] filename to read lines from * $p['cmd'] cmd to read lines from * $p['data'] data to read line from. either array of lines without newlines or string */ function __construct($p = array()) { if (array_key_exists('data', $p)) $this->lines = is_array($p['data']) || $p['data'] === null ? (array)$p['data'] : explode("\n", rtrim($p['data'], "\n")); else if (array_key_exists('cmd', $p)) $this->lines = strlen($data = it::exec($p['cmd'], $p['args'])) ? explode("\n", rtrim($data, "\n")) : array(); # NOPHPLINT else foreach ((array)($p['fn'] ? $p['fn'] : "php://stdin") as $fn) $this->lines = array_merge((array)$this->lines, ($t = it::file($fn, FILE_IGNORE_NEW_LINES)) === false ? array() : $t); } /** * Apply any php function to every line in pipe. Pass line as first arg except with some functions (e.g. explode) */ function __call($name, $params) { $parampos = self::$parampositions[$name] ?: 0; array_splice($params, $parampos, 0, [null]); # create room for inserted line arg $func = ($t = it::match('(\w+)__(\w+)', $name)) ? [$t[0], $t[1]] : (method_exists($this, $name) ? [$this, $name] : $name); foreach($this->lines as $i => $params[$parampos]) $this->lines[$i] = $func(...$params); return $this; } function __toString() { return is_array($this->lines) ? implode("\n", $this->lines) . "\n" : ""; } # # Internal: Implement iterator # #[ReturnTypeWillChange] function rewind() { $this->_position = 0; } #[ReturnTypeWillChange] function next() { $this->_position++; } #[ReturnTypeWillChange] function valid() { return array_key_exists($this->_position, $this->lines); } #[ReturnTypeWillChange] function current() { return $this->lines[$this->_position]; } #[ReturnTypeWillChange] function key() { return $this->_position; } /** * Apply an expression to every line */ function map($expr) { $this->lines = it::map($expr, $this->lines); return $this; } /** * Filter lines by an expression */ function filter($expr) { $this->lines = array_values(it::filter($expr, $this->lines)); return $this; } /** * Filter lines by a regexp */ function grep($regexp) { $this->lines = array_values(it::grep($regexp, $this->lines)); return $this; } /** * Select cols from tab-separated cols in each line and tab-joins them again. Key order relevant. */ function cut($picks) { foreach ($this->lines as $idx => $line) $this->lines[$idx] = implode("\t", it::filter_keys(explode("\t", $line), $picks, ['reorder' => true])); return $this; } /** * Swap first two columns */ function swap() { foreach ($this->lines as $idx => $line) { list($col1, $col2) = explode("\t", $line, 2); $this->lines[$idx] = "$col2\t$col1"; } return $this; } /** * Return contents of pipe as key->val pair (key must be tab separated). */ function keyval() { foreach ($this->lines as $line) { list($key, $val) = explode("\t", $line, 2); $result[$key] = $val; } return (array)$result; } /** * Return contents of pipe as array keys with value true */ function askey() { foreach ($this->lines as $line) $result[$line] = true; return (array)$result; } function ED() { ED($this->lines); return $this; } /** * Convert every line into an object with named columns * @param collist comma separator column name list * @param separator split character ["\t"] */ function cols($collist, $separator = "\t") { $keys = explode(",", $collist); foreach ($this->lines as $idx => $line) { if (count($keys) == count($arr = explode($separator, $line))) $this->lines[$idx] = (object)array_combine($keys, $arr); else { $this->lines[$idx] = (object)[]; foreach ($keys as $i => $key) $this->lines[$idx]->$key = $arr[$i]; } } return $this; } /** * Return contents of pipe as associative records. Column titles read from first line, separators can be \t or ; or , * @param $p['forceschema'] ignore schema in file, replace it with this comma separated column list * @param $p['fixcolnames'] replace non-identifier chars in colum names * @param $p['allowmissingcols'] replace missing column values at end by null * @param $p['it_error'] error params * @return array of objects or empty array on format violations */ function csv($p = []) { $p = is_string($p) ? ['forceschema' => $p] : $p; $counts = count_chars($this->lines[0]); $splitchar = $counts[ord("\t")] ? "\t" : ($counts[ord(";")] > $counts[ord(",")] ? ";" : ","); $csvhead = it::replace(['^\x{FEFF}' => ''], array_shift($this->lines)); # it::replace removes utf8 byte order mark $schema = $p['forceschema'] ? explode(",", $p['forceschema']) : str_getcsv(trim($csvhead, "#\n "), $splitchar, '"'); # should function_exists('str_getcsv') foreach ($schema as $idx => $col) $schema[$idx] = it::replace(['^$' => "field$idx", '\W' => $p['fixcolnames'] ? "_" : '$0'], $col); $oldlocale = setlocale(LC_CTYPE, "0"); setlocale(LC_CTYPE, 'de_CH.iso-8859-1'); # this works for utf-8 as well $nullcols = $p['allowmissingcols'] ? array_fill(0, count($schema), null) : []; foreach ($this->lines as $line) { if (($cols = str_getcsv($line, $splitchar, '"')) && (count($schema) == count($cols) || $nullcols)) $records[] = (object)array_combine($schema, $cols + $nullcols); else return (array)it::error((array)$p['it_error'] + ['title' => "csv column count mismatch: " . count($schema) . " in header vs " . count($cols) . " in row", 'body' => [$schema, $cols]]); } setlocale(LC_CTYPE, $oldlocale); return (array)$records; } /** * Return contents of pipe as array of lines */ function lines() { return $this->lines; } /** * Pipe our contents through a shell command. * @param $cmd command to execute, with {foo} for parameters * @param $args assoc array of arguments, see it::exec */ function pipe($cmd, ...$args) { $cmd = it::shell_command($cmd, ...$args); # NOPHPLINT $descriptors = array(0 => array("pipe", "r"), 1 => array("pipe", "w")); $process = proc_open($cmd, $descriptors, $pipes); fwrite($pipes[0], implode("\n", $this->lines) . ($this->lines ? "\n" :"")); fclose($pipes[0]); $out = stream_get_contents($pipes[1]); $this->lines = $out === "" ? [] : explode("\n", rtrim($out, "\n")); fclose($pipes[1]); proc_close($process); return $this; } /** * Save our contents in a file * @param $fn filename to save in, omit or null for stdout * @param $append append to file (boolean) */ function write($fn = null, $append = false) { it::file_put_contents($fn === null ? "php://stdout" : $fn, $this->lines ? implode("\n", $this->lines) . "\n" : "", $append ? FILE_APPEND : 0); return $this; } }