incrfilter is a Tcl prototype that provides full text searching of the contents of one or more files. Files can be specified on the command line or on stdin.
The program requires either one or more filenames, or the word "stdin" (which causes it to read the stdin file descriptor), reads the files specifies, takes the text entered in the small text widget at the top of the IncrFilter window, and searches for the lines that contain the text. The gimmick is that the search is done as the user types each character. The second, context, window, is used to display the context of the match. Click on a specific match line in the IncrFilter display and the surrounding text will be displayed.
The program accepts several flags.
Usage: incrfilter -v -l +l {-p list} {-f filter} file1 [file2 ...] [-]
| -f | allows one to specify a filter program as a second argument, which is then executed with the current file on its stdin and incrfilter reads the resulting stdout. |
| -p | indicates that searches should return a paragraph and not just a line |
| -l | file mode - return the name of the file that contains the hit |
| +l | line mode - return the line that contains the hit (default) |
| - | read data from stdin |
| -v | verbose mode - currently only displays the file being loaded |
The following code was written back in 1998 by JCW and Alexandre Ferrieux based on comments and feedback by Larry Virden. Further optimizations would be appreciated.
The idea behind this was this - in the olden days (back when I was a boy...) the people who had released Visicalc had a product (whose name escaped me) where searches took place in real time. The more data you added to the database, the faster you got to the data, because as you typed, the app would read more and more data as it spun under the extremely slow disk head, and by noting sectors which matched the keystrokes entered to date, could find the hit faster than waiting until the entire search term was entered and then searching all the sectors.
Features needed include:
Bugs include:
#! /bin/sh
#untcl\
exec tclsh $0 -- "$@"
# --------------------- History --------------------------------
# Authors: Jean-Claude Wippler, Alexandre Ferrieux, Larry Virden
# Purpose: return one or more lines that match string typed into entry widget
# default returns the line where hit occurs
# application expects you to provide either one or more files on the
# command line or data via stdin.
# Usage: incrfilter -v -l +l {-p <list>} {-f filter} file1 [file2 ...] -
# -f allows one to specify a filter program as a second argument
# -p indicates that searches should return a paragraph and not just a line
# -l file mode - return the name of the file that contains the hit
# +l line mode - return the line that contains the hit (default)
# - read data from stdin
# -v verbose mode
# v0.00 --- (Flat multi-grep.)
# Added interruptible search
# v0.05 ---
# Added context window, highlighting, and +/-l,-f modes
# v0.10 ---
# Replaced 'string match' by 'string first'. Faster ! (bad
# perf with regexp; this delays JC's \<)
# Moved -f's filter to visualization only. (So we match raw
# data only).Faster ! (esp. useful with "nroff -man | col -b")
# v0.15 ---
# Added bottom status label. Improved visual feedback.
# Added key bindings to both windows to help navigation
# v0.16 ---
# Added color highlighting
# Added paragraph (-p {list of strings}) mode
# v0.165 ---
# Added internal expansion of wildcards (for Windows)
# Added <Control-x><Control-f> to interactively open new files
# on platforms with no command line arguments (MacOS).
# v0.17
# v0.18 ---
# Prepared for dropping into Tcler's Wiki
# v0.19 --- (current)
# Added default switch settings and worked on stdin support
# v0.20
# added Tk dependency and defined control-u to clear the text in the search term entry widget
# -------------------- init ---------------------------------------------
set lmode line
set master {}
set idx 0
set running 0
set ent {}
# -------------------- GUI setup ----------------------------------------
set lab Loading
package require Tk
wm title . IncrFilter
entry .e -textvariable ent
pack .e -side top -fill x
label .l -textvariable lab -justify left -font [.e cget -font]
pack .l -side bottom -anchor w
frame .f
pack .f -side bottom -fill both -expand yes
text .f.t -yscrollcommand {.f.s set} -setgrid 1 -width 80 -state disabled
scrollbar .f.s -command {.f.t yview}
pack .f.s -side right -fill y
pack .f.t -side left -expand 1 -fill both
focus .e
bind .e <Control-x><Control-c> exit
bind .e <Up> {moveline -1}
bind .e <Down> {moveline 1}
bind .e <Control-u> {set ent [list ]}
bind .e <Control-x><Control-f> addfile
.f.t tag configure hilite -background black -foreground white
bind .f.t <Button-1> {clickline [lindex [split [.f.t index "@%x,%y linestart"] .] 0];break}
bind .f.t <B1-Motion> break
# --- early util ---
proc ssplitN {s tt} {
set l {}
while {[string length $s]} {
set z $s
set n 0
foreach t $tt {
set ii [string first $t $z]
if {$ii<0} continue
set z [string range $z 0 [expr {$ii-1}]]
set n [string length $t]
}
lappend l $z
set s [string range $s [expr {[string length $z]+$n}] end]
}
return $l
}
proc ssplit1 {s t} {
set n [string length $t]
set l {}
while {1} {
set ii [string first $t $s]
if {$ii<0} {
lappend l $s
break
}
lappend l [string range $s 0 [expr {$ii-1}]]
set s [string range $s [expr {$ii+$n}] end]
}
return $l
}
proc ssplit {s tt} {
if {[llength $tt] == 1} {ssplit1 $s [lindex $tt 0]} else {ssplitN $s $tt}
}
proc scopesplit {s mo} {
switch -glob -- $mo {
line {split $s \n}
file {list $s}
para:* {ssplit $s [string range $mo 5 end]}
default { ; }
}
}
proc swallowfile {f} {
global idx verbose lab master lmode para filter tab
if {[regexp -- {[\[*?]} $f]} {
foreach f2 [glob $f] {
swallowfile $f2
}
return
}
incr idx
if {$verbose} {puts stderr " (loading $f)"}
if {![expr {($idx-1) % 10}]} {
set lab "Loading $f"
update
}
if {$f ne "stdin"} {
set ff [open $f r]
} else {
set ff "stdin"
}
lappend master $idx
if {$lmode eq "para"} {append lmode :[subst -nocommands -novariables $para]}
set tab($idx,lmode) $lmode
set tab($idx,file) $f
set tab($idx,filter) $filter
set tab($idx,data) [scopesplit [read $ff] $lmode]
if {$f ne "stdin"} {
close $ff
}
}
proc modalentry {window title prompt destvar} {
global entryvar
global isok
upvar $destvar dest
catch {destroy $window}
set oldf [focus]
set isok 2
toplevel $window
raise $window
wm title $window $title
label $window.l -text $prompt -anchor w
append dest {}
set entryvar $dest
set okcmd {set isok 1}
set cancelcmd "set isok 0"
entry $window.e -width 40 -relief sunken -bd 2 -textvariable entryvar
$window.e selection range 0 end
bind $window.e <Return> "$okcmd;break"
bind $window.e <Escape> "$cancelcmd;break"
bind $window.e <Control-g> "$cancelcmd;break"
button $window.ok -text OK -command $okcmd
button $window.cancel -text Cancel -command $cancelcmd
pack $window.l $window.e $window.ok $window.cancel -side left -pady 5 -padx 5
update idletasks
focus $window.e
grab set $window
tkwait variable isok
grab release $window
focus $oldf
destroy $window
if {$isok} {set dest $entryvar}
return $isok
}
proc alert s {
tk_dialog .a Arrgh $s {} 0 Okay!
}
proc modedialog f {
global lmode para
set e +l
while {1} {
if {![modalentry .d Mode? "Mode for \"[file tail $f]\":" e]} {
return 0
}
switch -glob -- $e {
+l {set lmode line}
-l {set lmode file}
-p* {set lmode para;set para [lindex $e 1]}
default {alert "Bad mode: $e";continue}
}
return 1
}
}
proc addfile {} {
set f [tk_getOpenFile -title {Open File:}]
if {$f eq {}} return
if {[modedialog $f]} {
swallowfile $f
recalc
}
}
# --------------------- opts and args --------------------------------
proc usage {} {puts stderr {\
Usage: incrfilter [-v] [+l|-l|-p <list of strings>] [-f <filter>] [<file>] [-]}
exit 1
}
set para {}
set filter {}
set verbose 0
set want {}
set f stdin
foreach a $argv {
if {$want ne {}} {
set [lindex $want 0] $a
set want [lrange $want 1 end]
continue
}
switch -glob -- $a {
-f {set want filter;continue}
-p {set want para;set lmode para;continue}
-v {set verbose 1;continue}
-l {set lmode file;continue}
+l {set lmode line;continue}
- {set f stdin}
-* {usage}
default {set f $a}
}
}
swallowfile $f
switch [llength $master] {
0 {set tit IncrFilter}
1 {set tit $tab(1,file)}
default {set tit "$tab(1,file) ..."}
}
wm title . "IncrFilter $tit"
# --------------------- interaction ---------------------------------------
proc recalc {} {
global ent result lab
set lab "Filtering..."
update
if {![applyfilter $ent]} return
.f.t configure -state normal
.f.t delete 1.0 end
.f.t insert end [join $result \n]
.f.t configure -state disabled
}
proc tracer args {
global running interrupt
if {$running} {set interrupt 1} else {after idle recalc}
}
trace variable ent w tracer
proc allmatches {s pat} {
set len [string length $pat]
set l {}
set off 0
while {1} {
set res [string first $pat $s]
if {$res<0} break
lappend l [expr {$off+$res}] [expr {$off+$res+$len}]
set delta [expr {$res+$len}]
set s [string range $s $delta end]
incr off $delta
}
return $l
}
set oldidx nope
proc showctxwin {idx l pat} {
global tab oldidx
if {![winfo exists .ctx]} {
toplevel .ctx
if {!$idx} {lower .ctx .}
text .ctx.t -yscrollcommand ".ctx.s set" -setgrid 1 \
-width 80 -state disabled
scrollbar .ctx.s -command ".ctx.t yview"
pack .ctx.s -side right -fill y
pack .ctx.t -side left -expand 1 -fill both
foreach i {1 2 3 4 5} c {red blue green brown black} {
.ctx.t tag configure hilite$i -background $c -foreground white
}
bind .ctx <Control-x><Control-c> exit
bind .ctx <Up> {.ctx.t yview scroll -1 units}
bind .ctx <Down> {.ctx.t yview scroll 1 units}
bind .ctx <Prior> {.ctx.t yview scroll -1 pages}
bind .ctx <Meta-v> {.ctx.t yview scroll -1 pages}
bind .ctx <Next> {.ctx.t yview scroll 1 pages}
bind .ctx <Control-v> {.ctx.t yview scroll 1 pages}
}
.ctx.t configure -state normal
if {$oldidx != $idx} {
set oldidx $idx
.ctx.t delete 1.0 end
set reload 1
} else {
foreach t [.ctx.t tag names] {
.ctx.t tag remove $t 1.0 end
}
set reload 0
}
.ctx.t configure -state disabled
wm title .ctx Context
if {!$idx} return
wm title .ctx "Context in $tab($idx,file)"
set lmode $tab($idx,lmode)
.ctx.t configure -state normal
if {$tab($idx,filter) eq {}} {
set data $tab($idx,data)
} else {
update
set raw [exec sh -c \
"$tab($idx,filter);exit 0" \
< $tab($idx,file) 2>@ stderr]
set data [scopesplit $raw $lmode]
}
if {$reload} {
.ctx.t insert end [join $data \n]
}
switch -glob -- $lmode {
line {
set ii $l.0
set xx [lindex $data [expr {$l-1}]]
}
file {
set ii 1.0
set xx [lindex $data 0]
}
para:* {
set ss [string length [join [lrange $data 0 [expr {$l-2}]] \n]]
if {$l>1} {incr ss}
set ii [.ctx.t index "1.0 +${ss}c"]
set xx [lindex $data [expr {$l-1}]]
}
default { ; }
}
set i1 {}
set ci 0
foreach pp $pat {
if {$ci<5} {incr ci}
foreach {i1 i2} [allmatches $xx $pp] {
.ctx.t tag add hilite "$ii + ${i1}c" "$ii + ${i2}c"
.ctx.t tag add hilite$ci "$ii + ${i1}c" "$ii + ${i2}c"
}
}
.ctx.t configure -state disabled
if {[catch {
.ctx.t see [lindex [.ctx.t tag nextrange hilite $ii] 0]
}]} {
.ctx.t tag add hilite5 $ii "$ii + 1l"
.ctx.t see $ii
}
}
proc clickline l {
global result2 ent clickedline
set clickedline $l
.f.t tag remove hilite 1.0 end
if {!$l} {showctxwin 0 0 {};return}
set ab [lindex $result2 [expr {$l-1}]]
.f.t tag add hilite $l.0 "$l.0 + 1l"
update
showctxwin [lindex $ab 0] [lindex $ab 1] $ent
.f.t see $clickedline.0
}
proc moveline off {
global result clickedline
incr clickedline $off
if {$clickedline<1} {set clickedline 1}
set nn [llength $result]
if {$clickedline>$nn} {set clickedline $nn}
clickline $clickedline
}
# ----------------------- real meat --------------------------------------
proc applyfilter {f} {
global running interrupt result result2 master tab lab
set lmas [llength $master]
set result {}
set result2 {}
set tot 0
set cnt 0
set t [lindex [time {
foreach idx $master {
set lmode $tab($idx,lmode)
if {$lmas>1} {set pref "$tab($idx,file):\t"} else {set pref {}}
set lnum 0
foreach x $tab($idx,data) {
incr tot
incr lnum
if {![expr {$tot % 200}]} {
set running 1
set interrupt 0
update
set running 0
if {$interrupt} {
after idle recalc
return 0
}
}
set ok 1
#set x [string tolower $x]
foreach pat $f {
if {0>[string first $pat $x]} {set ok 0;break}
}
if {$ok} {
incr cnt
switch -glob -- $lmode {
para:* {
lappend result "${pref}($lnum)"
lappend result2 "$idx $lnum"
}
line {
lappend result "$pref$x"
lappend result2 "$idx $lnum"
}
file {
lappend result "$tab($idx,file)"
lappend result2 "$idx 0"
}
default { ; }
}
}
}
}
}] 0]
set lab "$cnt items. ([expr {$t/1000}] ms)"
clickline 0
return 1
}
# ------------------------ Go ! ------------------------------------------
recalc