Last active
July 10, 2021 18:19
-
-
Save rudiedirkx/543212 to your computer and use it in GitHub Desktop.
Search for/in (certain) files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 = ' '; | |
} | |
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