0

I have a knowledge base (aka Zettelkasten) of markdown files. And I use searching with the next command grep -irn 'search request' *.md. All works fine.

But I'd like see output with the title of the file and sub header of the found string.

The sample

file.md

1 # Title
2
3 ## Subtitle
4
5 yada-yada

Output

> grep -irn 'yada' *.md

< file.md:5:Title:Subtitle:yada-yada

Can I do this with grep without multiple search in the file?

An analogue of recall for terminal would be ideal.

1 Answer 1

0

No, grep can't do that. However, you could easily write your own custom search tool with awk or perl. e.g.

find ./ -name '*.md' -exec awk -v search="yada-yada" '
  /^# /  { title=$0;    sub(/^# /,"",title)     };
  /^## / { subtitle=$0; sub(/^## /,"",subtitle) };
  $0 ~ search { printf "%s:%i:%s:%s:%s\n", FILENAME,FNR,title,subtitle,$0 }' {} +

This is a very primitive example, and could be customised to suit your exact needs, and otherwise greatly improved. It should probably be wrapped in a shell script so you can have something like -v search="$1" rather than hard-coding it to "yada-yada".

Here's a slightly better version, in perl. This one doesn't require find (it uses perl's own File::Find module instead) and is possibly easier to extend with better option handling (e.g. you could support searching multiple directories similar to find, or add -i or -v options for case-insensitivity or to invert the match, same as grep and some other programs):

#!/usr/bin/perl

use strict;
use File::Find;

# Very primitive argument handling, should use Getopt::Long or
# one of the other Getopt::* modules
my $path   = shift;    # first arg is dir to search, e.g. './'
my $search = shift;    # second arg is regex to search for

find({ wanted => \&wanted, no_chdir => 1}, $path);

sub wanted {
  # This uses \z in the regex rather than just $ because
  # filenames can contain newlines.
  next unless (-f $File::Find::name && /\.md\z/s);
  
  # open the file and "grep" it.
  open(my $fh, "<", $File::Find::name) || warn "couldn't open $File::Find::name: $!\n";
  
  my $title    = '';
  my $subtitle = '';
  
  while(<$fh>) {
    chomp;
    
    if (/^# /) {
      ($title = $_) =~ s/^# //;
    
    } elsif (/^## /) { 
      ($subtitle = $_) =~ s/^## //;
    
    } elsif (/$search/) {
      printf "%s:%i:%s:%s:%s\n", $File::Find::name, $., $title, $subtitle, $_;
      
      # uncomment the next line if you want only the first match in
      # any given file (i.e. same as '-m 1' with grep):
      # close $fh;
    }
  };
  close($fh);
}

Sample run:

$ ./grep-md.pl ./ yada-yada
./file.md:5:Title:Subtitle:yada-yada
./file2.md:5:Another Title:And Another Subtitle:yada-yada
./sub/dir/several/levels/deep/file3.md:5:Third Title:File Three Subtitle:yada-yada

BTW, this could also be written to use find ... -exec to find the files rather than doing it with File::Find, and would probably be better if it did....I mostly wrote it this way to show multiple different ways of achieving the same end goal.

You must log in to answer this question.

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