1

I have a table with many rows and a variable number of columns per row.

In each row, I only want to print the first field and all the fields containing one of two strings (in this example, I want all fields containing the words dog and cow).

For example:

A   dog999   dog284   cow284   pig383   pig234   cow432   chicken432
B   cow394   cow432   cow345   dog983   pig345   chicken532 
C   dog847   pig357   pig236   cow395   dog496
D   dog392   cow237   cow749

Desired output:

A   dog999   dog284   cow284   cow432   
B   cow394   cow432   cow345   dog983   
C   dog847   cow395   dog496
D   dog392   cow237   cow749

So far with awk I have:

awk -v OFS='\t' '{for (i = 1; i <= NF; i++) {if ($i ~ /dog/) print $1,$i; else if ($i ~ /cow/) print $1,$i} }' file.txt

But that results in one line for every field that contains one of those two strings.

1
  • Use printf instead of print and add print "" outside of your loop.
    – user14755
    Sep 20, 2016 at 0:42

3 Answers 3

2

You were close, but you needed to extract the first value, 'cos you don't want to print that for each matching word. We can use printf to avoid newlines.

awk '{printf "%s",$1
      for (i=1;i<=NF;i++)
      {
        if ($i ~ /dog|cow/) { printf " %s",$i; }
      }
      print ""
     }'

The output would be:

A dog999 dog284 cow284 cow432
B cow394 cow432 cow345 dog983
C dog847 cow395 dog496
D dog392 cow237 cow749

This can be collapsed to one line:

awk '{printf "%s",$1; for (i=1;i<=NF;i++) { if ($i ~ /dog|cow/) { printf " %s",$i; }  } print ""  }'

Note that this will print a line that doesn't match any words eg

E pig sheep

will output

E
2

If perl solution is fine:

$ cat ip.txt 
A   dog999   dog284   cow284   pig383   pig234   cow432   chicken432
B   cow394   cow432   cow345   dog983   pig345   chicken532 
C   dog847   pig357   pig236   cow395   dog496
D   dog392   cow237   cow749

$ perl -lane 'print join("\t",$F[0],grep {/cow|dog/} @F[1..$#F])' ip.txt 
A   dog999  dog284  cow284  cow432
B   cow394  cow432  cow345  dog983
C   dog847  cow395  dog496
D   dog392  cow237  cow749
  • -a split input line on spaces and save to @F array
  • -l strip newlines from input and add back when printing
  • join will add a \t between elements when printing
  • $F[0],grep {/cow|dog/} @F[1..$#F] first element of array and all elements matching cow or dog
  • Can also use perl -lape'$_=join"\t",shift(@F),grep/cow|dog/,@F'. here shift will delete and return first element of @F array, assigning result to $_ will get printed at end courtesy -p option (Tip of hat to Stéphane Chazelas)


If lines not containing cow or dog are to be ignored:

perl -lane 'print join("\t",$F[0],grep {//} @F[1..$#F]) if /cow|dog/' ip.txt 
1
  • 1
    Or perl -lape'$_=join"\t",shift(@F),grep/cow|dog/,@F' if we want to golf it. Dec 11, 2017 at 11:12
0

TXR awk macro:

$ txr -e '(awk (:let tmp)
               (:begin (set ofs "\t"))                     
               (f (set tmp (pop f))
                  (ff (keep-if #/cow|dog/))
                  (push tmp f) (prn)))' data
A   dog999  dog284  cow284  cow432
B   cow394  cow432  cow345  dog983
C   dog847  cow395  dog496
D   dog392  cow237  cow749

Breakdown:

  1. The :let clause in the macro specifies local variables. This macro implements the "Awk Paradigm" but in a type-safe language, in which variables have to be defined before use. So in addition to clauses like :begin and :end (analogous to BEGIN and END in POSIX Awk), this Awk provides :let for defining variables that are lexically scoped to the macro.

  2. (f (set tmp (pop f)) ...) is a condition-action clause, where the condition is f. If is the list of delimited fields from the record; if it is not empty (not equal to nil) then it behaves like Boolean true. So the action forms execute if there is anything in f.

  3. (set tmp (pop f)) pops the first field from the list and saves it in the temporary variable tmp. The second field becomes first, the third second and so on. When we operate on f, the record rec is also reconstituted automatically using ofs, just like in POSIX Awk, the $0 record is reconstituted using OFS between the fields.

  4. (ff ...) filters the fields through an operation, in this case (keep-if #/regex/). Basically we remove from f all the fields that don't match the regex. ff is an operator visible inside the awk macro. keep-if is a regular function; here it is implicitly curried, so the list argument doesn't appear. It expects a predicate function, but a regex is function-callable, so suitable as a predicate.

  5. Then we push back the previously saved first field onto the field list f with (push tmp f).

  6. (prn) is the equivalent of print. With no arguments, it prints the record, followed by the output record separator (ors) which is initialized to newline. Since rec has been reconstituted after all the manipulations of f, we get the filtered output.

As can be seen, the Awk paradigm is basically intact, just in the context of a different language in which different kinds of things are possible. The convenience of just being able to do $2 > $1 without checking that these fields actually exists is not there; but on the other hand, we don't have to write loops to process the fields as a data structure. Fields can be mapped through functions or treated as a stack.

Sundeep's Perl solution roughly translates to the awk macro like this:

$ txr -e '(awk (t (prn `@[f 0]\t@{(keep-if #/cow|dog/ [f 1..:]) "\t"}`)))' data

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.