0

Say I have a file containing:

⟫ cat schema.rb 
  create_table "things", id: :serial, force: :cascade do |t|
    t.string "other_column"
    # ...
    t.datetime "created_at"
  end

  create_table "users", id: :serial, force: :cascade do |t|
    t.citext "email"
    # ...
    t.datetime "created_at", precision: 0
  end

and I want to find all lines matching created_at but not matching precision:. That's easy enough:

⟫ grep created_at schema.rb 
    t.datetime "created_at"
    t.datetime "created_at", precision: 0

⟫ grep created_at schema.rb | grep -v precision:
    t.datetime "created_at"

but what if I want to get some context lines for the matched lines so I can see which create_table block they occurred in? Adding a -C/-B flag to the final grep -v is too late because the first grep already dropped all context lines.

⟫ grep created_at schema.rb | grep -v precision: -B3
    t.datetime "created_at"

But adding it to the first grep is too early because the grep -v only removes its matched line, not the context lines around its matched lines:

⟫ grep created_at -B3 schema.rb | grep -v precision: -B3
  create_table "things", id: :serial, force: :cascade do |t|
    t.string "other_column"
    # ...
    t.datetime "created_at"
--
  create_table "users", id: :serial, force: :cascade do |t|
    t.citext "email"
    # ...

Is there any way to get it to only include the context lines for the matched lines from the first grep (or equivalently, get the grep -v to remove the context lines around its matched lines)?

  create_table "things", id: :serial, force: :cascade do |t|
    t.string "other_column"
    # ...
    t.datetime "created_at"

Or is there another command that would do this for me?

(a simple sed script perhaps—if it requires anything more than a simple sed script, I may as well write it in ruby so that it's easier to read and maintain).

  • If you want blank-line separated records matching / not matching the two strings you could just use awk in paragraph mode e.g. awk -vRS= '/created_at/ && !/precision/' schema.rb – steeldriver Aug 15 '18 at 17:21
  • Interesting idea, thanks! Yes, that does look like it would work in this case since the create_table sections are all separated by a blank line. – Tyler Rick Aug 15 '18 at 17:28
1

I don't think what I was trying to do is possible if I chain together 2 grep commands (since the context lines are relative to each individual grep command).

It occurred to me that a negative lookahead might be what I want. Then it could all be done with a single grep command.

To my surprise, it looks like GNU grep actually does have support for regex lookbehind/lookahead — but only if you use the --perl-regex option.

Here's a grep command that gives me what I was looking for:

⟫ grep --perl-regexp 'created_at(?!(.*precision:))' schema.rb -B3
  create_table "things", id: :serial, force: :cascade do |t|
    t.string "other_column"
    # ...
    t.datetime "created_at"
0

awk solution

$ awk '/create_table/,/created_at/&&!/precision:/' schema.rb
  create_table "things", id: :serial, force: :cascade do |t|
    t.string "other_column"
    # ...
    t.datetime "created_at"
  create_table "users", id: :serial, force: :cascade do |t|
    t.citext "email"
    # ...
    t.datetime "created_at", precision: 0
  end
$

None of these below achieve exactly what you're after, but I'll throw them in, in case useful in some way.

sed solution that prints the create_table line and the match

Look for create_table line, if found, store in holding buffer 'h'. Look for created_at followed by precision, if found, ignore it. Look for created_at alone, if found, print it.

$ sed -n '/create_table/h;/created_at.*precision:/d;/created_at/{H;g;p}' schema.rb
  create_table "things", id: :serial, force: :cascade do |t|
    t.datetime "created_at"
$

awk solution that prints the create_table line and the match

If you specifically wanted the create_table line against the match, you could use

$ awk '/create_table/{t=$0}/created_at/&&!/precision:/{print t"\n"$0}' schema.rb
  create_table "things", id: :serial, force: :cascade do |t|
    t.datetime "created_at"
$

awk solution that prints the table

Taking it a little further, if you wanted just the table names which had a matching line within, you could use

$ awk '/create_table/{t=$2}/created_at/&&!/precision:/{print substr(t,2,length(t)-3)}' schema.rb
things
$
0

Wouldn't simply reversing the two greps do what you want, eliminating the precision lines upfront so they won't match the created_at lines in the second grep?

grep -v precision schema.rb | grep -A1 -B3 created_at 
create_table "things", id: :serial, force: :cascade do |t|
   t.string "other_column"
   # ...
   t.datetime "created_at"
 end

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.