Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I have a file with many rows, and each row has a timestamp at the starting, like

[Thread-3] (21/09/12 06:17:38:672) logged message from code.....

So, I frequently check 2 things from this log file.

  1. First few rows, that has the global conditions and start time is also given.
  2. Last few rows, that has the exit status with some other info.

Is there any quick handy single command that could let me display just the first and last few lines of a file?

share|improve this question
2  
What's global conditions, and doesn't head and tail works for you? –  warl0ck Sep 21 '12 at 9:40
    
That is the part of my log file. I was trying to be elaborative. You can ignore that. –  mtk Sep 21 '12 at 10:00
    
Your solution looks fine to me. If you want more convenience, make it into a shell function (even an alias might do). –  vonbrand Feb 28 '13 at 14:33
    
@vonbrand Problem is that I don't know N –  Bernhard Feb 28 '13 at 14:36
    
@Bernhard, I'm no sed(1) expert, but there are ways of stashing stuff away for later use with it. Maybe it pays off to look in there. OTOH, I'd probably whip up a Perl (or whatever) script to do it if used frequently, as I'm more familiar with that. –  vonbrand Feb 28 '13 at 14:41

8 Answers 8

up vote 11 down vote accepted

You can use sed or awk to make it with one command. However you'll loose at speed, cause sed and awk will need to run through the whole file anyway. From a speed point of view it's much better to make a function or every time to combination of tail + head. This does have the downside of not working if the input is a pipe, however you can use proccess substitution, in case your shell supports it (look at example below).

first_last () {
    head -n 10 -- "$1"
    tail -n 10 -- "$1"
}

and just launch it as

first_last "/path/to/file_to_process"

to proceed with process substitution (bash, zsh, ksh like shells only):

first_last <( command )

ps. you can even add a grep to check if your "global conditions" exist.

share|improve this answer
    
-n 10 is the default, no? –  l0b0 Mar 1 '13 at 9:54
    
@l0b0 yes, it's default. -n 10 is not necessary here. –  rush Mar 1 '13 at 10:35

@rush is right about using head + tail being more efficient for large files, but for small files (< 20 lines), some lines may be output twice.

{ head; tail;} < /path/to/file

would be equally efficient, but wouldn't have the problem above.

share|improve this answer
    
In contrast to rushs solution, this does not work in a POSIX shell. –  Marco Feb 28 '13 at 14:51
2  
@Marco Huh? Only POSIX constructs are used here. What do you see going wrong? –  Gilles Feb 28 '13 at 15:10
2  
@Gilles I missed the space: {head; tail;} < file works in zsh but fails in sh. { head; tail;} < file always works. Sorry for the noise. –  Marco Feb 28 '13 at 15:39
    
@Marco, if there were problems with that, it would be with head, not the shell. POSIX requires head to leave the cursor in the file just past those 10 lines for regular files. A problem could arise for non-POSIX head implementations (very old versions of GNU head used to be non-conformant in that instance, but we're talking decades) or if the file is not seekable (like named pipe or socket, but then the other solution would have the same problem). –  Stéphane Chazelas Feb 28 '13 at 15:41
    
For multiple files we for f in *; do { head; tail;} < $f; done –  Atcold Mar 24 at 19:56

The { head; tail; } solution wouldn't work on pipes (or sockets or any other non-seekable files) because head could consume too much data as it reads by blocks and can't seek back on a pipe potentially leaving the cursor inside the file beyond what tail is meant to select.

So, you could use a tool that reads one character at a time like the shell's read (here using a function that takes the number of head lines and tail lines as arguments).

head_tail() {
  n=0
  while [ "$n" -lt "$1" ]; do
    IFS= read -r line || { printf %s "$line"; break; }
    printf '%s\n' "$line"
    n=$(($n + 1))
  done
  tail -n "${2-$1}"
}
seq 100 | head_tail 5 10
seq 20 | head_tail 5

or implement tail in awk for instance as:

head_tail() {
  awk -v h="$1" -v t="${2-$1}" '
    {l[NR%t]=$0}
    NR<=h
    END{
      n=NR-t+1
      if(n <= h) n = h+1
      for (;n<=NR;n++) print l[n%t]
    }'
}

With sed:

head_tail() {
  sed -e "1,${1}b" -e :1 -e "$(($1+${2-$1})),\$!{N;b1" -e '}' -e 'N;D'
}

(though beware that some sed implementations have a low limitation on the size of their pattern space, so would fail for big values of the number of tail lines).

share|improve this answer

Using bash process substitution, you can do the following:

make_some_output | tee >(tail -n 2) >(head -n 2; cat >/dev/null) >/dev/null

Note that the lines are not guaranteed to be in order, though for files longer than about 8kB, they very likely will be. This 8kB cutoff is the typical size of the read buffer, and is related to the reason | {head; tail;} doesn't work for small files.

The cat >/dev/null is necessary to keep the head pipeline alive. Otherwise tee will quit early, and while you'll get output from tail, it'll be from somewhere in the middle of the input, rather than the end.

Finally, why the >/dev/null instead of, say, moving tail to another |? In the following case:

make_some_output | tee >(head -n 2; cat >/dev/null) | tail -n 2  # doesn't work

head's stdout is fed into the pipe to tail rather than the console, which isn't what we want at all.

share|improve this answer
    
When head or tail finish writing the output they want, they close their stdin and exit. That's where the SIGPIPE is coming from. Normally this is a good thing, they're discarding the rest of the output, so there is no reason for the other side of the pipe to continue spending time generating it. –  derobert Feb 28 '13 at 16:12
    
What makes the order likely to be upheld? It probably will be for a large file, because tail has to work longer, but I expect (and do see) it failing about half the time for short inputs. –  Gilles Feb 28 '13 at 16:20
    
You'll get the SIGPIPE with tee >(head) >(tail) for the same reasons (>(...) which by the way is a ksh feature now supported by both zsh and bash as well) uses pipes as well. You could do ... | (trap '' PIPE; tee >(head) >(tail) > /dev/null) but you'll still see some broken pipes error messages from tee. –  Stéphane Chazelas Feb 28 '13 at 16:28
    
On my system (bash 4.2.37, coreutils 8.13), tail is the one being killed by SIGPIPE, not tee, and tail isn't writing to a pipe. So it must be from a kill(), right?. And this only happens when I'm using the | syntax. strace says that tee isn't calling kill()... so maybe bash? –  Jander Feb 28 '13 at 16:38
1  
@Jander, try feeding more than 8k like seq 100000 | tee >(head -n1) >(tail -n1) > /dev/null –  Stéphane Chazelas Feb 28 '13 at 17:39

Stephane's first solution in a function so that you can use arguments (works in any Bourne-like or POSIX shell):

head_tail() {
    head "$@";
    tail "$@";
}

Now you can do this:

head_tail -n 5 < /path/to/file

This of course assumes that you're looking at only one file and like Stephane's solution works (reliably) only on regular (seekable) files.

share|improve this answer

Using ed (which will read the entire file into RAM though):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' 'H' '1,10p' '$-10,$p' 'q' | ed -s file
share|improve this answer

You may try PERL, if you have it installed:

perl -e '@_ = <>; @_=@_[0, -3..-1]; print @_'

This will work for most files, but reads the whole file into memory before processing it. If you're not familar with PERL slices, "0" in square brackets means "take the first line", and "-3...-1" means "take last three lines". You may tailor both of them to your needs. If you need to process really large files (what is 'large' may depend on your RAM and perhaps swap sizes), you may want to go for:

perl -e 'while($_=<>){@_=(@_,$_)[0,-3..-1]}; print @_'

it may be somewhat slower, because it makes a slice every iteration, but it's independent on the file size.

Both commands should work both in pipes and with regular files.

share|improve this answer

I ran into something like this today where I needed only the last line and a few lines from the front of a stream and came up with the following.

sed -n -e '1{h}' -e '2,3{H}' -e '${H;x;p}'

I read this as: initialize the hold space with the contents of the first line, append lines 2-3 in the hold space, at EOF append the last line to the hold space, swap hold-and-pattern space, and print the pattern space.

Perhaps someone with more sed-fu than I have can figure out how to generalize this to print the last few lines of the stream indicated in this question but I didn't need it and could not find an easy way to do math based on the $ address in sed or perhaps by managing the hold space so that only the last few lines are in it when EOF is reached.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.