Find and edit matching files

As a programmer, sometimes I find myself doing some repetitive thing and eventually it occurs to me to automate that thing. Now is one of those moments, and the repetitive thing is searching a code base for a symbol and opening one of the matches in my favorite text editor.

The sequence usually goes like this:

  • (comes across symbol myFunction) Oh! I wonder where that lives?
  • (opens another tab in screen) grep -rnsI myFunction
  • Oh! it lives in widgets/retro_encabulator.js!
  • vim widgets/retro_encabulator.js
  • /myFunction<enter>
  • so on…

So in about a half hour I came up with this:

$ ~/Documents/scripts/ccss cmd *.js
 1: ./web/skins/classic/views/js/watch.js:398:function cmdDisableAlarms()
    ./web/skins/classic/views/js/watch.js:403:function cmdEnableAlarms()
    ./web/skins/classic/views/js/watch.js:408:function cmdForceAlarm()
    ./web/skins/classic/views/js/watch.js:413:function cmdCancelForcedAlarm()
 2: ./web/skins/classic/views/js/montage.js:115:    requestQueue.addRequest( "cmdReq"+this.id, this.streamCmdReq );

 Anything else: Cancel

The script takes a text to search, and an optional second parameter being file type. The results are listed with each unique file being numbered, and user input is taken to select which file to open. Choosing a file opens vim to the line of the first match occurrence.

And the script:

#!/bin/bash
# Search all folders and give editing options
#

if [ $# -lt 1 ]; then
    echo "Usage: $0 searchstring [filetype]"
    echo "Example: $0 myFunction *.php"
    exit
fi

if [ $# -lt 2 ]; then
    PARM='*'
else
    PARM="$2"
fi

TMP=`mktemp`
find -name "$PARM" -type f |xargs -I {} grep -nsHI $1 {} |awk -F: '!a[$1]++{printf "%2d: ", ++num} b[$1]++{printf "    "}{gsub("t", ""); print substr($0,1,'`tput cols`'-4)}' > $TMP
more $TMP
echo
echo " Anything else: Cancel"
for i in $(seq 1 $((`egrep '^([0-9]| )[0-9]' $TMP |wc -l |wc -c`-1))); do
    read -n 1 chr
    if [ $i -eq 1 ] && [[ ! "$chr" =~ [1-9] ]]; then
        rm $TMP
        echo
        exit
    fi
    chz="$chz""$chr"
done
echo
FLTE=`egrep '^[ ]*'$chz':' $TMP |awk -F: '{printf $2}'`
if [ ${#FLTE} -gt 1 ]; then
    vim +`egrep '^[ ]*'$chz':' $TMP |awk -F: '{printf $3}'` $FLTE
fi
rm $TMP

A few scripting techniques appear here. First off, the help text on lines 5 to 9. Lines 11 to 15 choose all files (“*”) if no file specifier is given. Line 18 finds matching files, prints line numbers for the first occurrence of a file with !a[$1]++{printf "%2d: ", ++num} and whitespace otherwise with b[$1]++{printf " "}. Output is also cropped to terminal width with print substr($0,1,'`tput cols`'-4)}.

The list is output using more, after which one or more characters are read from the keyboard. $((`egrep '^([0-9]| )[0-9]' $TMP |wc -l |wc -c`-1)) gives the number of digits in the greatest number selection, so that two or more numbers are read only if the selections exceed 9 choices; The test if [ $i -eq 1 ] && [[ ! "$chr" =~ [1-9] ]] makes sure to exit if the first character isn’t the beginning of one of the numerical selections.

Finally, the file to edit is picked from the output line with egrep '^[ ]*'$chz':' $TMP, printing the second field in the colon-delimited list (being the filename). So long as this was a choice originally presented to the user, vim is called with the line number (retrieved with egrep '^[ ]*'$chz':' $TMP |awk -F: '{printf $3}').

As with many scripting projects, this is a great start and will see immediate use. Future improvements include passing multiple file extension matches, and modifier keys to the menu allowing one to preview the lines of code surrounding the matched line in one of the choices.