Skip to content

Instantly share code, notes, and snippets.

@rudiedirkx
Last active July 10, 2021 18:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rudiedirkx/543212 to your computer and use it in GitHub Desktop.
Save rudiedirkx/543212 to your computer and use it in GitHub Desktop.
Search for/in (certain) files
<?php
class Filesearch {
// Config
public $string = '';
public $extensions = array();
public $dir = '';
public $exclude = array();
public $in_files = true;
public $matcher = 'ci';
public $padding = 2;
// Process
public $match_extensions = array();
public $matcher_callback = null;
public $regex_string = '';
public $file_contents = array();
public $_start = 0;
public $_end = 0;
// Result
public $num_scanned_files = 0;
public $num_matched_files = 0;
public $num_matched_lines = 0;
public $matched_files = array();
public $matched_lines = array();
function __construct( $dir, $string, $options = array() ) {
foreach ( $options as $name => $value ) {
if ( isset($this->$name) ) {
$this->$name = $value;
}
}
if ( is_string($this->extensions) ) {
$this->extensions = array_filter(explode(',', $this->extensions));
}
$this->exclude = array_filter(array_map('realpath', $this->exclude));
$this->dir = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $dir);
$this->string = $string;
}
function find() {
$this->_start = microtime(1);
switch ($this->matcher) {
case 're':
case 'reci':
$ci = $this->matcher == 'reci' ? 'i' : '';
$this->matcher_callback = function($string, $pattern) use ($ci) {
return preg_match('/' . $pattern . "/$ci", $string) > 0;
};
$this->regex_string = '/(' . $this->string . ")/$ci";
break;
case 'cs':
$this->matcher_callback = function($haystack, $needle) {
return strpos($haystack, $needle) !== FALSE;
};
$this->regex_string = '/(' . preg_quote($this->string, '/') . ')/';
break;
case 'ci':
default:
$this->matcher_callback = function($haystack, $needle) {
return stripos($haystack, $needle) !== FALSE;
};
$this->regex_string = '/(' . preg_quote($this->string, '/') . ')/i';
break;
}
$this->match_extensions = in_array('*', $this->extensions) ?: array_flip(array_map('strtolower', $this->extensions));
set_time_limit(0);
$this->read($this->dir);
$this->_end = microtime(1);
$this->printStatsSummary();
}
function dirContents( $dir ) {
$files = array();
$map = opendir($dir);
while ( $file = readdir($map) ) {
$filepath = $dir . DIRECTORY_SEPARATOR . $file;
if ( is_readable($filepath) && $file[0] != '.' ) {
$files[] = $filepath;
}
}
natcasesort($files);
return $files;
}
function read( $dir ) {
$dir = rtrim($dir, '\\/');
foreach ( $this->dirContents($dir) as $filepath ) {
if ( is_file($filepath) ) {
if ( $this->potentialFile($filepath) ) {
$this->num_scanned_files++;
if ( $this->match($filepath) ) {
// match
$this->num_matched_files++;
$this->matched_files[] = $filepath;
$this->matched_lines[$filepath] = array();
$this->printFileMatch($filepath);
}
}
}
else if ( is_dir($filepath) ) {
if ( $this->potentialDir($filepath) ) {
// recurse
$this->read($filepath);
}
}
}
}
function getStatsTitle() {
$_end = $this->_end ?: microtime(1);
$time = number_format($_end - $this->_start, 2);
$delim = in_array($this->matcher, ['re', 'reci']) ? '/' : '"';
return $this->num_matched_lines . ' x <code>' . $delim . html($this->string) . $delim . '</code> in ' . $this->num_matched_files . ' / ' . $this->num_scanned_files . ' files (' . $time . ' sec)';
}
function printStatsSummary() {
echo '<script>';
echo 'document.getElementById("top-results-summary").innerHTML = "' . javascript($this->getStatsTitle()) . '";';
echo 'document.title = "' . javascript(strip_tags($this->getStatsTitle())) . '";';
echo '</script>';
}
function htmlClass( $string ) {
$string = strtolower($string);
$string = preg_replace('#[^a-z0-9\-]+#', '-', $string);
$string = trim($string, '-');
$string = preg_replace('#\-+#', '-', $string);
return $string;
}
function printFileMatch( $filepath ) {
$linesBefore = $this->num_matched_lines;
$lineMatches = '';
if ( $this->in_files ) {
$lineMatches .= '<div class="match-groups">';
$lineMatches .= $this->printLineMatches($filepath);
$lineMatches .= '</div>';
}
$linesAfter = $this->num_matched_lines;
$matches = $linesAfter - $linesBefore;
$numMatches = $this->in_files ? ' (' . $matches . ')' : '';
echo "\n" . '<div class="match-file">';
echo '<h3 id="' . $this->htmlClass($filepath) . '" class="match-file-title">' . $filepath . $numMatches . '</h3>' . "\n";
echo $lineMatches;
echo $this->printStatsSummary();
echo '</div>' . "\n\n";
flush();
// sleep(1);
}
function printLineMatches( $filepath ) {
$lines = $this->getFileLines($filepath);
$matchedGroups = $this->getLineMatches($filepath);
$html = '';
foreach ( $matchedGroups as $group ) {
$matchedLine = key($group);
$linesPre = array_slice($lines, 0, $matchedLine);
$codePre = implode("\n", $linesPre);
$lastFunction = '<no function or class>';
if ( preg_match_all('#(function\s+[a-zA-Z0-9_]+[\sa-zA-Z0-9,_&:\(\)$\[\]=\'"-\.]*)\s*{#', $codePre, $functionMatches) ) {
$lastFunction = trim(end($functionMatches[1]));
}
else if ( preg_match_all('#(class\s+[a-zA-Z0-9_]+[\sa-zA-Z0-9,_&:\(\)$\[\]=\'"-]*)\s*{#', $codePre, $functionMatches) ) {
$lastFunction = trim(end($functionMatches[1]));
}
$html .= '<div class="match-function-context">' . html($lastFunction) . '</div>';
$html .='<ul class="match-group">';
foreach ( $group as $line => $match ) {
$classes = $match ? 'match-line-match' : '';
$printLine = $lines[$line];
if ( $printLine === '' ) {
$printLine = '&nbsp;';
}
else if ( $match ) {
$i = preg_match_all($this->regex_string, $printLine, $rematches, PREG_OFFSET_CAPTURE);
$rematches = $rematches[0];
$parts = array();
$lastEnd = 0;
foreach ($rematches as $rematch) {
$parts[] = html(substr($printLine, $lastEnd, $rematch[1] - $lastEnd));
$parts[] = '<span class="match">' . html($rematch[0]) . '</span>';
$lastEnd = $rematch[1] + strlen($rematch[0]);
}
$parts[] = html(substr($printLine, $lastEnd));
$printLine = implode($parts);
}
else {
$printLine = html($printLine);
}
$lineNumber = str_pad($line + 1, 4, ' ', STR_PAD_LEFT);
$lineNumber = '<span class="ln">' . $lineNumber . ' </span>';
$html .='<li class="match-line ' . $classes . '">' . $lineNumber . $printLine . '</li>';
}
$html .='</ul>';
}
return $html;
}
function getLineMatches( $filepath, $context = null ) {
$context === null && $context = $this->padding;
$matcher = $this->matcher_callback;
$lines = $this->getFileLines($filepath);
$maxLine = count($lines) - 1;
$matchedLines = array();
foreach ( $lines as $i => $line ) {
if ( $matcher($line, $this->string) ) {
$this->num_matched_lines++;
$this->matched_lines[$filepath][] = $i;
if ( $context ) {
for ( $j=max(0, $i-$context), $m=min($maxLine, $i+$context); $j<=$m; $j++ ) {
$matchedLines[$j] = !empty($matchedLines[$j]) || $i == $j;
}
}
else {
$matchedLines[$i] = true;
}
}
}
$matchedGroups = array();
$lastLine = -9;
foreach ( $matchedLines as $line => $match ) {
if ( $line != $lastLine+1 ) {
$matchedGroups[] = array();
$group = &$matchedGroups[count($matchedGroups)-1];
}
$group[$line] = $match;
$lastLine = $line;
}
return $matchedGroups;
}
function getFileContents( $filepath ) {
if ( !isset($this->file_contents[$filepath]) ) {
$this->file_contents[$filepath] = file_get_contents($filepath);
}
return $this->file_contents[$filepath];
}
function getFileLines( $filepath ) {
$contents = $this->getFileContents($filepath);
return preg_split('/(\r\n|\r|\n)/', $contents);
}
function potentialFile( $filepath ) {
$name = basename($filepath);
return $name[0] != '.' && $this->matchExtension($name) && !in_array(realpath($filepath), $this->exclude);
}
function matchExtension( $name ) {
if ($this->match_extensions === true) {
return true;
}
$ext = strtolower(substr(strrchr($name, '.'), 1));
return isset($this->match_extensions[$ext]);
}
function potentialDir( $filepath ) {
$name = basename($filepath);
return $name[0] != '.' && !in_array(realpath($filepath), $this->exclude);
}
function match( $filepath ) {
return $this->matchFileName($filepath) || ( $this->in_files && $this->matchFileContents($filepath) );
}
function matchFileName( $filepath ) {
$matcher = $this->matcher_callback;
return $matcher($filepath, $this->string);
}
function matchFileContents( $filepath ) {
$matcher = $this->matcher_callback;
$contents = file_get_contents($filepath);
$match = $matcher($contents, $this->string);
if ( $match ) {
$this->file_contents[$filepath] = $contents;
}
return $match;
}
}
header('Content-type: text/html; charset=utf-8');
if ( isset($_GET['extensions']) && $_GET['extensions'] != @$_COOKIE['fs_extensions'] ) {
$_COOKIE['fs_extensions'] = $_GET['extensions'];
setcookie('fs_extensions', $_COOKIE['fs_extensions'], time()+86400*90);
}
if ( isset($_GET['path']) && $_GET['path'] != @$_COOKIE['fs_path'] ) {
$_COOKIE['fs_path'] = $_GET['path'];
setcookie('fs_path', $_COOKIE['fs_path'], time()+86400*90);
}
if ( isset($_GET['exclude']) ) {
$excludes = explode(',', (string) @$_COOKIE['fs_excludes']);
$excludes = array_filter(array_unique(array_merge($excludes, (array) $_GET['exclude'])));
sort($excludes);
$excludes = implode(',', $excludes);
if ( $excludes != @$_COOKIE['fs_excludes'] ) {
setcookie('fs_excludes', $_COOKIE['fs_path'] = $excludes, time()+86400*90);
}
}
$matchers = array('ci' => 'Case insensitive', 'cs' => 'Case sensitive', 're' => 'RegExp', 'reci' => 'RegExp CI');
$paddings = array(0 => 'none', 1 => '1 line', 2 => '2 lines', 3 => '3 lines', 5 => '5 lines', 10 => '10 lines');
$excludes = array_filter(explode(',', (string) @$_COOKIE['fs_excludes']));
$excludeDirs = array_filter((array)@$_GET['exclude']) ?: array('');
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="/favicon.png" />
<style>
html, body { margin: 0; padding: 0; width: -webkit-fit-content; min-width: 100%; }
body { padding: 6px 0; }
p { margin: 2px 8px; }
label { display: inline-block; min-width: 7em; }
input:not([type=checkbox]):not([type=submit]):not([type=reset]) { width: 40em; }
.exclude a { display: inline-block; margin-left: .5em; text-decoration: none; -webkit-transform: scale(1.8); font-weight: bold; }
h3, ul, li { margin: 0; padding: 0; display: block; list-style: none; }
.match-file { margin-bottom: 8px; }
.match-file-title { font-family: monospace; font-size: 1.2rem; font-weight: normal; background-color: #444; color: #fff; padding: 3px 6px; }
.match-groups { font-family: monospace; background-color: #888; }
.match-function-context { color: white; }
.match-function-context:first-child { padding-top: 4px; }
.match-group { padding: 4px 0; line-height: 1.4; }
.match-line { white-space: pre; tab-size: 4; background-color: #ccc; border-bottom: solid 1px rgba(0, 0, 0, 0.06); }
.match-line-match { background-color: #ddd; position: relative; }
span.match { background-color: #f88; color: #000; padding: 2px; }
span.ln { color: #d63900; }
.result-options { text-align: right; padding: 4px 8px; }
#top-results-summary { float: left; }
body:not(.ln) .ln { display: none; }
body.fn .match-file > :not(h3) { display: none; }
</style>
</head>
<body class="ln">
<form id="search_form" method="get" action>
<p><label>String: </label><input id="string" name="string" value="<?= html(@$_GET['string']) ?>" /></p>
<p><label>Extensions: </label><input name="extensions" value="<?= html(@$_COOKIE['fs_extensions']) ?>" /></p>
<p><label>Path: </label><input name="path" value="<?= html(@$_COOKIE['fs_path']) ?>" /></p>
<?php foreach ( $excludeDirs as $exclude ) { ?>
<p class="exclude"><label>Ignore: </label><input autocomplete="off" list="excludes" name="exclude[]" value="<?= html($exclude) ?>" /> <a class="more-exclude" href="#">+</a></p>
<?php } ?>
<p class="cb"><label><input type="checkbox" name="in_files" value="1" checked /> Within files</label></p>
<p><label>Find method: </label><select name="matcher"><?= options($matchers, isset($_GET['matcher']) ? $_GET['matcher'] : 'cs') ?></select></p>
<p><label>Show context: </label><select name="padding"><?= options($paddings, isset($_GET['padding']) ? $_GET['padding'] : 10) ?></select></p>
<datalist id="excludes"><option value="<?= implode('"><option value="', $excludes) ?>"></datalist>
<p>
<input type="submit" />
<input type="reset" />
</p>
</form>
<script>
function $(sel) { return document.querySelector(sel); }
function fillExcludes(path) {
<? if (!empty($_GET['excludes'])): ?>return console.log('no excludes');<? endif ?>
var oldEl;
while (oldEl = $('p.exclude + p.exclude')) {
oldEl.remove();
}
var added = false;
[].forEach.call($('#excludes').options, el => {
if (el.value.startsWith(path)) {
var newEl = addExclude();
newEl.querySelector('input').value = el.value;
added = true;
}
});
if (added) {
$('p.exclude').remove();
}
}
function addExclude() {
var els = document.querySelectorAll('p.exclude');
var lastEl = els[els.length-1];
var newEl = lastEl.cloneNode(true);
newEl.querySelector('input').value = '';
lastEl.parentNode.insertBefore(newEl, lastEl.nextElementSibling);
return newEl;
}
$('#search_form input[name="path"]').onchange = function(e) {
fillExcludes(this.value);
};
$('#search_form').onclick = function(e) {
if (e.target.classList.contains('more-exclude')) {
e.preventDefault();
var newEl = addExclude();
setTimeout(function() {
newEl.querySelector('input').focus();
});
}
};
var $path = $('input[name="path"]');
if ($path.value) {
fillExcludes($path.value);
}
</script>
<?php
if ( isset($_GET['string'], $_GET['path']) ) {
?>
<div class="result-options" style="max-width: calc(100vw - 20px); box-sizing: border-box">
<div id="top-results-summary"></div>
<label><input type="checkbox" onclick="document.body.classList.toggle('fn', this.checked)" /> Show ONLY file names</label>
<label><input type="checkbox" onclick="document.body.classList.toggle('ln', this.checked)" checked /> Show line numbers</label>
</div>
<?php
$_GET['in_files'] = !empty($_GET['in_files']);
$search = new Filesearch($_GET['path'], $_GET['string'], $_GET);
// print_r($search);
$search->find();
$time = number_format($search->_end - $search->_start, 4);
echo '<p>' . $search->getStatsTitle() . '</p>';
// print_r($search);
}
?>
</body>
</html>
<?php
function javascript( $str ) {
return addslashes($str);
}
function html( $str ) {
return htmlspecialchars($str, ENT_COMPAT, 'UTF-8');
}
function options( $options, $selected ) {
$html = '';
foreach ( $options AS $v => $label ) {
$html .= '<option value="' . html($v) . '"';
if ( $selected == $v ) {
$html .= ' selected';
}
$html .= '>' . html($label) . '</option>';
}
return $html;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment