2

I'm trying to write a function which will search through a file in the same manner that reverse-search-history works. i.e. user starts typing in, prompt is updated with 1st match, hitting a special key rotates through other matches, hitting another special key selects the current match.

I wrote a bash script to this, but it is awfully slow. Was wondering if I could harness some other unix/bash feature to make this fast. Maybe using awk?

Any ideas would be appreciated.

For this script, TAB rotates through matches, ENTER selects the current match, ESC ends, BACKSPACE removes the last character in the current search. (forgive my dodgy bash script, am relatively new to bash/unix)

#!/bin/bash


do_search()
{
        #Record the current screen position
        tput sc
        local searchTerm
        local matchNumber=1
        local totalSearchString
        local TAB_CHAR=`echo -e "\t"`

        #print initial prompt
        echo -n "(search) '':"

        #-s: means input will not be echoed to screen, -n1 means that one character will be gotten
        while IFS= read -r -s -n1 char
        do
                #If ENTER
                if [ "$char" == "" ]; then
                        if [ "$match" != "" ]; then
                                eval "$match"
                        fi
                        echo ""
                        return 0

                #If BACKSPACE
                elif [ "$char" == "" ]; then
                        if [ "$totalSearchString" != "" ]; then
                                totalSearchString=${totalSearchString%?}
                        fi

                #If ESCAPE
                elif [ "$char" == "" ]; then
                        tput el1
                        tput rc
                        return 0

                #If TAB
                elif [ "$char" == "$TAB_CHAR" ]; then
                        matchNumber=$(($matchNumber+1))

                #OTHERWISE
                else
                        totalSearchString="$totalSearchString$char"
                fi

                match=""
                if [ "$totalSearchString" != "" ]; then
                        #This builds up a list of grep statements piping into each other for each word in the totalSearchString
                        #e.g. totalSearchString="blah" will output "| grep blah"
                        #e.g. totalSearchString="blah1 blah2" will output "| grep blah1 | grep blah2"
                        local grepStatements=`echo $totalSearchString | sed 's/\([^ ]*\) */| grep \1 /g'`
                        local cdHistorySearchStatement="cat $1 $grepStatements | head -$matchNumber | tail -1"

                        #Get the match
                        match=`eval "$cdHistorySearchStatement"`
                fi

                #clear the current line
                tput el1
                tput rc

                #re-print prompt & match
                echo -n "(search) '$totalSearchString': $match"
        done
  return 0
}

do_search categories.txt
Ben
  • 6,567
  • 10
  • 42
  • 64

2 Answers2

1

I think bash uses readline for this, why don't you look into using it yourself? I don't know much more about it, sorry, but I thought it might help.

static_rtti
  • 53,760
  • 47
  • 136
  • 192
  • And the readline commands came from (drum roll) ... EMACS. So your bash script should just contain something like `emacs -nw $1` ;-) – flolo Apr 12 '11 at 08:07
  • Yes, readline is often used for what you are attempting to do. For example, see the sqsh shell, for sybase commands (see http://www.sqsh.org), it will be much smaller and easier to understand than emacs (I think). Sqsh is a compiled 'C' program that links in the readline c library to provide history navigations. Maybe there is a standalone readline that you can use in a shell? Good Luck! – shellter Apr 12 '11 at 12:59
  • I've had a look into readline. Not sure if it's really what I'm after. I can see how you can program custom completers, but a) I'm not really keen on getting into programming in C, and b) completion (as in showing list of possiblilities) is not really what I'm after. If I've misunderstoon the capabilities of readline then please correct me. – Ben Apr 13 '11 at 07:33
1

I don't think this can be made fast enough for interactive use in pure bash (maybe using the complete builtin?). That said, you can try simplifying the commands you're using. Instead of one grep per word, you can use one for all of them, by doing

  grepStatements=$(echo "$totalSearchString" | sed 's/[ ]\+/|/g')
  cdHistorySearchStatement="grep '$grepStatements' $1 | ..."

and instead of head -$matchNumber | tail -1 you could use sed -n ${matchNumber}{p;q}.

Idelic
  • 14,976
  • 5
  • 35
  • 40