95

I have text file. Task - get first and last line from file after

$ cat file | grep -E "1|2|3|4" | commandtoprint

$ cat file
1
2
3
4
5

Need this without cat output (only 1 and 5).

~$ cat file | tee >(head -n 1) >(wc -l)
1
2
3
4
5
5
1

Maybe awk and more shorter solution exist...

chaos
  • 48,171
dmgl
  • 1,083

7 Answers7

169

sed Solution:

sed -e 1b -e '$!d' file

When reading from stdin if would look like this (for example ps -ef):

ps -ef | sed -e 1b -e '$!d'
UID        PID  PPID  C STIME TTY          TIME CMD
root      1931  1837  0 20:05 pts/0    00:00:00 sed -e 1b -e $!d

head & tail Solution:

(head -n1 && tail -n1) <file

When data is coming from a command (ps -ef):

ps -ef 2>&1 | (head -n1 && tail -n1)
UID        PID  PPID  C STIME TTY          TIME CMD
root      2068  1837  0 20:13 pts/0    00:00:00 -bash

awk Solution:

awk 'NR==1; END{print}' file

And also the piped example with ps -ef:

ps -ef | awk 'NR==1; END{print}'
UID        PID  PPID  C STIME TTY          TIME CMD
root      1935  1837  0 20:07 pts/0    00:00:00 awk NR==1; END{print}
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
chaos
  • 48,171
  • 3
    Thank you! It's the best answer because i cannot do (head -n1 file;tail -n1 file) i have very big command and pipe as last symbol. So | sed '1p;$!d' shorter one. – dmgl Jun 25 '14 at 11:09
  • Sorry for my English, if you don't understand me - it's my problem - tell me about it and i prepare better presentation for you. – dmgl Jun 25 '14 at 11:12
  • @StéphaneChazelas Thank you for this Note! Demo: file with only 1. Command $ cat file | sed '1p;$!d' #return 1 1 and $ cat file | awk 'NR==1; END{print}' return 1 1 – dmgl Jun 25 '14 at 11:18
  • Can we generalise this if we replace 'file' by an arbitrary process writing to stdout? – gerrit Jun 25 '14 at 14:56
  • @gerrit i added your suggestions – chaos Jun 25 '14 at 16:18
  • @chaos - though in almost every situation it would not matter, there are certain rare cases in which you might need to ensure you always get the first and last line directly following one another. With sed you can do 1h;$!d;x;G;1g - that way the output of the first and last lines will always instantaneously follow one another, and in the event of a single line input you'll always only get the singe line out. – mikeserv Jun 25 '14 at 16:42
  • Can you explain the sed one(s)? Even after a look at the sed man page, I'm still confused (this is a common occurrence for me with the sed man page). – David Conrad Jun 25 '14 at 21:58
  • 13
    Does head && tail really work for you? seq 10 | (head -n1 && tail -n1) prints 1 only. – glenn jackman Jun 26 '14 at 10:16
  • @glennjackman That annoyed me too a bit. The head && tail part is not always working. Switch to line buffered stream, then it works: stdbuf -oL seq 10 | (head -n1 && tail -n1) print 1 and 10 for me. – chaos Jun 26 '14 at 14:29
  • @DavidConrad -e 1b means match line number 1 continue -e '$!d' means: $ match the last line, ! invert and d delete. So: match everything but the last line and delete it. – chaos Jun 26 '14 at 14:50
  • 2
    @DavidConrad, to elaborate on what chaos said, -e 1b -- on the first line, branch to the end of the sed script, at which point the implicit "print" happens. If the input is only one line long, sed ends. Otherwise, for all lines except the last, delete. On the last line, the implicit "print" happens again. – glenn jackman Jun 26 '14 at 19:10
  • @mikeserv: Why do you use 1g at the end of your sed script ? Using it crushes the contents of the pattern space with that of the hold space, which is just the last line. Shouldn't you be already well served by seq 10 | sed '1h;$!d;x;G' ? – Cbhihe Sep 18 '16 at 10:35
  • @cbhihe - not sure - just on my phone w/ 3% charge and only a single sock, but i would guess it is to prevent getting the first line twice if input is only a single line – mikeserv Sep 19 '16 at 21:52
  • 1
    @mikeserv: ;-) check it when you get a chance. On a single line it efectively prevents doubling it on output, but on a multiple line work-case, it just preserves the last line... at least on GNU sed 4.2.2. Cheers. – Cbhihe Sep 19 '16 at 23:15
  • @cbhihe - you know i would check it, but the CVPD has my computer on a $150 hock and the rest of my things burned up with my house when they torched it. its a weir'd life. – mikeserv Sep 19 '16 at 23:56
  • @cbhihe - that expression does $!d... so the only case when a 1g should crush the output is a case of a oneline input file, barring a continued live scenario with more than one last line... – mikeserv Jan 17 '19 at 23:52
  • @mikeserv: yup. agreed ! But talk about long lived thread and necroposting... 4 yrs and going strong. Hope all yr woes from back in Sep.'16 are behind you. – Cbhihe Jan 18 '19 at 09:46
  • @cbhihe - the mad hatter said unbirthdays make us young again. the dormouse helped him drink his wine. but i say, if ! kill -0 $$; then ! $0 -ba</; fi – mikeserv Jul 27 '19 at 00:42
  • @glennjackman try with (head -n1 && tail -n1) <<< $(seq 10) – crsuarezf Mar 09 '22 at 05:23
34

sed -n '1p;$p' file.txt will print 1st and last line of file.txt .

schaiba
  • 7,631
11

A funny pure Bash≥4 way:

cb() { (($1-1>0)) && unset "ary[$1-1]"; }
mapfile -t -C cb -c 1 ary < file

After this, you'll have an array ary with first field (i.e., with index 0) being the first line of file, and its last field being the last line of file. The callback cb (optional if you want to slurp all lines in the array) unsets all the intermediate lines so as to not clutter memory. As a free by-product, you'll also have the number of lines in the file (as the last index of the array+1).

Demo:

$ mapfile -t -C cb -c 1 ary < <(printf '%s\n' {a..z})
$ declare -p ary
declare -a ary='([0]="a" [25]="z")'
$ # With only one line
$ mapfile -t -C cb -c 1 ary < <(printf '%s\n' "only one line")
$ declare -p ary
declare -a ary='([0]="only one line")'
$ # With an empty file
$ mapfile -t -C cb -c 1 ary < <(:)
declare -a ary='()'
10

With sed you could delete lines if NOT the 1st one AND NOT the la$t one.
Use ! to NOT (negate) a condition and the X{Y..} construct to combine X AND Y conditions:

cmd | sed '1!{$!d;}'

or you could use a range - from 2nd to la$t - and delete all lines in that range except the la$t line:

cmd | sed '2,${$!d;}'
don_crissti
  • 82,805
8

Without cat:

$ cat file |tee >(head -n1) >(tail -n1) >/dev/null
1
5

or

$ (head -n1 file;tail -n1 file)
1
5
7

Using Perl:

$ seq 10 |  perl -ne 'print if 1..1 or eof'
1
10

The above prints the first item in the output of seq 10 via the if 1..1, while the or eof will also print the last item.

JigglyNaga
  • 7,886
  • 3
    Could you explain how this works? Please avoid giving such one-liner answers without an explanation of what they do and how. – terdon Jun 25 '14 at 18:35
  • 1
    what do /etc abrt and yum.repos.d have to do this question? – drs Jun 25 '14 at 18:48
  • @drs I edited the answer to avoid using 'ls' as sample input. – dolmen Jun 26 '14 at 08:58
  • 1
    @dolmen: might be a good idea to finish editing by suppressing all reference to /etc. I guess such was added after yr first edit, but, all the same, as it stands the answer makes no sense at all. Just a suggestion. – Cbhihe Sep 18 '16 at 08:42
  • Good answer. I used it for an alias: alias firstlast='perl -ne "print if 1..1 or eof"'. Now I can seq 10|firstlast. With $. containing the line number, replacing 1..1 with $.==1 might be more readable for those unfamiliar with Perl's flip-flop dot-dot operator. – Kjetil S. Aug 05 '21 at 09:58
5
$ seq 100 | { IFS= read -r first; echo "$first"; tail -1; }
1
100
glenn jackman
  • 85,964