8

I'm currently using

watch head -n 17 *

which works, but also shows all lines up to the 17th. Basically, I would like to only show the last line for each file that is shown with my current approach. How can I achieve that?

Example

For the sake of example, let's reduce the line nr. to 7. So:

Example file:

1
2
3
4
5
6
7
8

this line:

watch head -n 7 *

outputs

1
2
3
4
5
6
7

where I want:

7
0

3 Answers 3

15

With GNU awk:

watch -x gawk '
  FNR == 17 {nextfile}
  ENDFILE   {if (FNR) printf "%15s[%02d] %s\n", FILENAME, FNR, $0}' ./*

Which gives an output like:

        ./file1[17] line17
  ./short-file2[05] line 5 is the last

Note that the ./* glob is expanded only once at the time watch is invoked.

Your watch head -n 17 * was an arbitrary command injection vulnerability as the expansion of that * was actually interpreted as shell code by the shell that watch invokes to interpret the concatenation of its arguments with spaces.

If there was a file called $(reboot) in the current directory, it would reboot.

With -x, we're telling watch to skip the shell and execute the command directly. Alternatively, you could do:

watch 'exec gawk '\''
  FNR == 17 {nextfile}
  ENDFILE   {if (FNR) printf "%15s[%02d] %s\n", FILENAME, FNR, $0}'\'' ./*'

For watch to run a shell which would expand that ./* glob at each iteration. watch foo bar is in effect the same as watch -x sh -c 'foo bar'. When using watch -x, you can specify which shell you want and for instance pick a more powerful one like zsh that can do recursive globbing and restrict to regular files:

watch -x zsh -c 'awk '\''...'\'' ./**/*(.)'

Without gawk, you could still do something like:

watch '
  for file in ./*; do
    [ -s "$file" ] || continue
    printf "%s: " "$file"
    head -n 17 < "$file" | tail -n 1
  done'

Giving an ouput like:

./file1: line17
./short-file2: line 5 is the last

But that would be a lot less efficient as it implies running several commands per file.

1
  • Thanks for the security note. Actually you're right and my command also didn't do what I needed it to do, i.e. watch added files, which your version also solves. The only downside is that I need to learn AWK, I had though this would be done with find . -exec or sth like this :D Aug 21, 2019 at 14:44
11

Combine head and tail like in these two examples:

$ seq 1 80 | head -n 17 | tail -n 1
17

$ seq 1 10 | head -n 17 | tail -n 1
10

So to solve your actual problem, command is:

watch 'for f in *; do head -n 17 -- "$f" 2>/dev/null | tail -n 1 ; done'

Note about the 2>/dev/null part, it is needed because * will match directories and any files you might not have permission to read, which will produce error message, which you probably want to hide.

0
0

another approach with find,exec and sed

watch find . -type f -name \"*\" -exec sh -c \'\( printf '%-50s ' {} \; sed -n 17p {}\)\' \\\;
1
  • 1
    Doesn't work for files with fewer than 17 lines. Read the question carefully.
    – Wildcard
    Aug 21, 2019 at 18:05

You must log in to answer this question.

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