4

I want to search all defined functions in bash for some search string. The below is a start, but I want to then eliminate all terms that are not followed by whitespace on the next line (i.e. eliminate all entries that did not find $1 in the body of that function).

fu() { declare -f | grep -e \(\) -e $1; }

e.g. This output:

...
tt ()
untargz ()
urlfix ()
ver ()
    [ -f /etc/lsb-release ] && RELEASE="$(cat /etc/lsb-release | grep DESCRIPTION | sed 's/^.*=//g' | sed 's/\"//g') ";
vi. ()
vi.su ()
...

would reduce to

...
ver ()
    [ -f /etc/lsb-release ] && RELEASE="$(cat /etc/lsb-release | grep DESCRIPTION | sed 's/^.*=//g' | sed 's/\"//g') ";
...

An even much much better way (if possible) would be if every matching function could be determined and displayed in full.

I envision that roughly as:

  • Collect the names of the functions with the search string in their body (the name of the function is always a single word on a line before the match, starting at ^ followed by a space then the line ending with ()$), then using command -V on each of those names, OR, doing a declare -f again but this time, using those names and matching everything after them from { to } (where { and } are on single lines by themselves at ^ - I know that grep/awk/sed can do amazing things to those that have such knowledge.

End result would be running fu awk and it will show me the definition of every function that contains awk in the body of the function.

0

2 Answers 2

5

The following awk command on the receiving end of the pipe comes to mind:

declare -f | awk -v srch="pattern" 'NF==2 && $2=="()"{if (m) print buf; buf=""; m=0}
                                    buf && index($0,srch){m=1}
                                    {buf=buf ORS $0}
                                    END{if (m) print buf}'

The idea is to store the declaration and body of each function in a buffer buf while parsing the output of declare -f, but only print the buffer if the search string was found.

  • The program will recognize the start of a new function definition if it encounters a line consisting of only two fields (= space-separated "words"), where the second field is (). If a match was found while parsing the previous function (indicated by the flag m being 1), the buffer buf will be printed. Both the buffer and the flag will be reset.
  • The search word is passed to the program as awk variable srch. If it is found on the current line (the index function returns a non-zero result), the m flag is set to 1, but only if we are not on the line where the function declaration starts (indicated by buf not being empty), otherwise matches in the function name would also count.
  • Every line will be appended to the buffer buf, and separated from the previous content by the output record separator ORS (which defaults to newline).
  • At the end, another check is performed if the match was found, and if so, the buffer printed. Without that check the last function definition would never be considered.

Note

The program performs a full-string search by using the index() function of awk. If you want the search to be based on regular expression matching, you would need to change the condition from

index($0,srch)

to

$0~srch

(but, as always, be aware that searching for strings that contain characters which have special meanings for regular expressions becomes more cumbersome).

1
  • Incredible, and thanks for the detailed explanation, awk is something that I find difficult, so it's great to learn this 🙂. My final function works a treat (it does use a 3rd party tool, but that's just something that I find a bit better than pymentize for visually scanning scripts). fu() { declare -f | awk -v srch="$1" 'NF==2 && $2=="()"{if (m) print buf; buf=""; m=0} index($0,srch){m=1} {buf=buf ORS $0} END{if (m) print buf}' | bat -pp -f -l bash; }
    – YorSubs
    Commented Oct 5, 2021 at 14:47
4

For those looking at doing the same thing in zsh, to get the function definitions of functions whose body matches a pattern, you'd do:

typeset -f '<dummy>' ${(k)functions[(R)pattern]}

the <dummy> to cover for the case where there's no matching function. Or a cleaner way:

() { (($#)) && typeset -f -- "$@"; } ${(k)functions[(R)pattern]}

In bash, you could do something similar with:

compgen -A function | (
  ret=1
  while IFS= read -r fn; do
    def=$(typeset -f -- "$fn") &&
      [[ ${def#*'()'} = pattern ]] &&
      printf '%s\n' "$def" &&
      ret=0
  done
  exit "$ret"
)

Though that's quite inefficient as the list of function names is read one byte at a time (as read's input is a pipe), and one process is forked for each function. It could be optimised with:

compgen -A function | (
  readarray -t functions
  for fn in "${functions[@]}"; do
    typeset -f -- "$fn" && printf '\0'
  done
) | (
  ret=1
  readarray -td '' fn_definitions
  for def in "${fn_definitions[@]}"; do
    [[ ${def#*'()'} = pattern ]] &&
      printf '%s\n' "$def" &&
      ret=0
  done
  exit "$ret"
)

readarray, contrary to read, doesn't need to read the input one byte at a time as it consumes all the input anyway. We delimit the function definitions with NUL characters as anyway bash (contrary to zsh) can't store that character in its function definitions/names nor variables contents and then we can use readarray -td '' (which requires bash-4.4+) to break that into an array.

That avoids having to rely on heuristics such as the presence of () (which could very well also occur in the body of a function) to guess where each function definition starts in the output of typeset -f.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .