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
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.

  • 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 – Felix Dombek Aug 21 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

another approach with find,exec and sed

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

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.