0

I want to read the prototype of a C function through the source code of a big project.

I know the function name, and its return type, and that its prototype will be defined in a *.h file.

I'd use grep(1), but I want to be able to read multi-line prototypes, so it's discarded.

So what I usually do is:

  • project: glibc
  • return type: int
  • function name: cacheflush
syscall='cacheflush';
find glibc/ -name '*.h' \
|xargs sed -n "/^[a-z ]*int ${syscall}[ ]*(/,/^$/p";

But that prints some unwanted lines after the ones I want:

$ find glibc/ -name '*.h' \
  |xargs sed -n "/^[a-z ]*int ${syscall}[ ]*(/,/^$/p";
extern int cacheflush (void *__addr, int __nbytes, int __op) __THROW;
#endif

extern int cacheflush (void *__addr, const int __nbytes,
               const int __op) __THROW;
#endif

extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;
#endif
extern int _flush_cache (char *__addr, const int __nbytes, const int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;
#endif
extern int _flush_cache (char *__addr, const int __nbytes, const int __op) __THROW;


I'd like to be able to replace the end pattern /^$/-> /;/, but then it'll only work when the function prototype spans across multiple lines. Is it possible to tell sed(1) that the end pattern might be on the same line as the begin pattern, so that the output would be the following?:

$ find glibc/ -name '*.h' | xargs sed magic;
extern int cacheflush (void *__addr, int __nbytes, int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes,
               const int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;


3

You could use pcregrep's multi-line mode:

$ pcregrep --include='\.h$' -rM '(?s)^\s*(\w+\s+)*int cacheflush\s*\(.*?;' glibc
glibc/sysdeps/unix/sysv/linux/mips/sys/cachectl.h:extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;
glibc/sysdeps/unix/sysv/linux/csky/sys/cachectl.h:extern int cacheflush (void *__addr, const int __nbytes,
                       const int __op) __THROW;
glibc/sysdeps/unix/sysv/linux/nios2/sys/cachectl.h:extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;

And with PCRE, you get access to most of perl's advanced regexp operators. Here, we use:

  • \w, and \s for word and whitespace characters.
  • (?s): enables the s flag for . to also match on newline characters.
  • *?: the non-greedy version of *. So it matches up to the first occurrence of ; and not the last like the greedy version would.

See the pcrepattern(3) man page for details.

3
  • I like this version especially because pcregrep, as grep, tells you the file and line where it found the pattern :). And also because the natural tool to search for a pattern is *grep
    – alx
    Dec 29 '20 at 21:02
  • How do (?s) and the last ? work? I didn't find it.
    – alx
    Dec 30 '20 at 14:09
  • 1
    @alx, see edit. Dec 30 '20 at 16:25
1

There is no need to invoke sed twice, you just check before entering the range, whether the begin/end happen to be on the same line.

$ find glibc/ -name '*.h' \
|xargs sed \
    -e "/${pattern}.*;\$/b"  \
    -e "/${pattern}/,/;\$/p" \
    -e 'd' ;

Note that it will be good if you constrain the find utility to locate regular files only, otherwise you might see warnings when sed operates upon a directory whose name ends in a .h

4
  • A minor bugfix: "/${pattern}/,/;\$/!d" --> "/${pattern}/,/;\$/"'!d' . For some reason I can't make !d work with ".
    – alx
    Dec 29 '20 at 20:59
  • 1
    That's coz the bang ! does not gel well under double quotes in bash. Your approach will work. I will provide sn alternative in the above. In bash 4.4 and above this issue is not seen.
    – guest_7
    Dec 29 '20 at 21:10
  • Thanks for the fix. However, I'm on Debian 11 with bash 5.1 and the original code didn't work.
    – alx
    Dec 29 '20 at 21:27
  • On that I am not sure as at my end gnu hash 4.4.23 on redhat linux it works.
    – guest_7
    Dec 29 '20 at 21:35
0

I've come up with a (ugly) solution:

  1. Read from /begin/ to /^$/ (blank line), repeating the first line of the pattern, so that sed(1) can act on it on a subsequent step.

  2. Use sed(1) to find from /begin/ to /end/. Keep blank lines at this step to be able to use uniq(1) to correctly remove on the next step the lines we repeated on step 1.

  3. Use uniq(1) to remove the repeated lines.

$ syscall=cacheflush;
$ return=int;
$ pattern="^[a-z ]*${return} ${syscall}[ ]*(";
$ find glibc/ -name '*.h' \
  |xargs sed -n -e "/${pattern}/p" -e "/${pattern}/,/^$/p" \
  |sed -n -e "/${pattern}/,/;/p" -e '/^$/p' \
  |uniq;
extern int cacheflush (void *__addr, int __nbytes, int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes,
               const int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;

extern int cacheflush (void *__addr, const int __nbytes, const int __op) __THROW;

Please, come with a simpler solution :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.