143

I am trying to get a specific line from a text file.

So far, online I have only seen stuff like sed, (I can only use the sh -not bash or sed or anything like that). I need to do this only using a basic shell script.

cat file | while read line
    do
       #do something
    done

I know how to iterate through lines, as shown above, but what if I just need to get the contents of a particular line

Josip Rodin
  • 725
  • 6
  • 13
GangstaGraham
  • 8,865
  • 12
  • 42
  • 60

13 Answers13

264

sed:

sed '5!d' file

awk:

awk 'NR==5' file
Kent
  • 189,393
  • 32
  • 233
  • 301
  • What about with the sh command, I cannot use sed, awk. I should make this more clear in the question. – GangstaGraham Oct 11 '13 at 21:45
  • @GangstaGraham you said you know how to iterate through lines, how about adding a counter? if the counter reaches your target line number, get the line and break the loop. does it help? – Kent Oct 11 '13 at 21:51
  • yeah, i guess so, sh in general is kind of confusing though – GangstaGraham Oct 11 '13 at 23:39
  • @Kent what is 5!d here ..? and how to use this lineNumebr as a variable in shell script ..? – Kanagavelu Sugumar Apr 24 '14 at 10:13
  • 4
    @KanagaveluSugumar read sed's info page. `5!d` means delete all lines except 5. shell var is possible, you need double quotes. – Kent Apr 24 '14 at 10:15
  • 29
    I would suggest adding another variant: `sed -n 5p` This seems more logical to remember for newbies, because `-n` means "no output by default" and `p` stands for "print", and there's no potentially confusing mention of deleting (when people talk of files, deleting lines tends to mean something different). – Josip Rodin Sep 08 '15 at 08:13
  • 3
    @JosipRodin you are right, `-n '5p'` works for this problem too. The difference here is, with `5!d` you can add `-i` to write the change back to the file. however, with `-n 5p` you have to `sed -n '5p' f > f2&& mv f2 f` again, for this question, I am agree with your opinion. – Kent Sep 08 '15 at 09:45
36

Assuming line is a variable which holds your required line number, if you can use head and tail, then it is quite simple:

head -n $line file | tail -1

If not, this should work:

x=0
want=5
cat lines | while read line; do
  x=$(( x+1 ))
  if [ $x -eq "$want" ]; then
    echo $line
    break
  fi
done
Victor Zamanian
  • 3,100
  • 24
  • 31
micromoses
  • 6,747
  • 2
  • 20
  • 29
  • This `-eq` comparison is for integers, so it wants a line number, not line content (`$line`). This has to be fixed by defining e.g. `want=5` prior to the loop, and then using the `-eq` comparison on `$want`. [moved from a rejected edit] – Josip Rodin Oct 31 '15 at 14:32
  • 1
    @JosipRodin I made an independent edit suggestion based on your comment, as I agree with it. Hopefully this time it won't be rejected. – Victor Zamanian Aug 02 '17 at 10:30
  • This is massively much slower than @faithonour 's solution below – AntonOfTheWoods Jun 05 '22 at 00:36
  • @AntonOfTheWoods while using `sed` is obviously faster, it does not provide an answer to the question. If you missed it, the question explicitly states that `sed` is not usable, and `sh` is the only tool available. – micromoses Jun 12 '22 at 17:59
  • True, he did accept the answer though! :-) – AntonOfTheWoods Jun 13 '22 at 16:29
31

You could use sed -n 5p file.

You can also get a range, e.g., sed -n 5,10p file.

Nomas Prime
  • 1,230
  • 11
  • 16
22

Best performance method

sed '5q;d' file

Because sed stops reading any lines after the 5th one

Update experiment from Mr. Roger Dueck

I installed wcanadian-insane (6.6MB) and compared sed -n 1p /usr/share/dict/words and sed '1q;d' /usr/share/dict/words using the time command; the first took 0.043s, the second only 0.002s, so using 'q' is definitely a performance improvement!

faithonour
  • 341
  • 2
  • 4
  • 2
    This is also commonly written: `sed -n 5q` – William Pursell Sep 09 '15 at 15:59
  • 3
    I like this solution because `sed` stops reading any lines after the 5th one. – Anthony Geoghegan Mar 09 '16 at 16:21
  • 2
    I installed wcanadian-insane (6.6MB) and compared `sed -n 1p /usr/share/dict/words` and `sed '1q;d' /usr/share/dict/words` using the `time` command; the first took 0.043s, the second only 0.002s, so using 'q' is definitely a performance improvement! – Roger Dueck Jul 04 '19 at 15:17
  • 2
    Warning: using `q` in a sed command from a pipe will result in `broken pipe`. In that case, have to resort to `sed -n 5p` – stevesliva Nov 18 '20 at 23:11
  • @WilliamPursell `sed -n 5q` prints nothing for me. I think the answer is the best variant, but `sed -n '5{p;q}'` also works for me. – jmou Sep 01 '22 at 22:48
12

If for example you want to get the lines 10 to 20 of a file you can use each of these two methods:

head -n 20 york.txt | tail -11

or

sed -n '10,20p' york.txt 

p in above command stands for printing.

Here's what you'll see: enter image description here

Mona Jalal
  • 34,860
  • 64
  • 239
  • 408
2

The standard way to do this sort of thing is to use external tools. Disallowing the use of external tools while writing a shell script is absurd. However, if you really don't want to use external tools, you can print line 5 with:

i=0; while read line; do test $((++i)) = 5 && echo "$line"; done < input-file

Note that this will print logical line 5. That is, if input-file contains line continuations, they will be counted as a single line. You can change this behavior by adding -r to the read command. (Which is probably the desired behavior.)

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • 1
    `$((++i))` appears to be a bashism; if the OP is restricted in using external tools, I wouldn't assume they'll have access to more than a plain `/bin/sh` – Josip Rodin Sep 08 '15 at 08:20
  • @JosipRodin No, it's a [POSIX](http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_04) feature (but support for `++` increments is specifically marked as optional). – tripleee Sep 08 '15 at 09:56
  • @tripleee it doesn't work with modern dash as /bin/sh, so I would not rely upon it. – Josip Rodin Sep 08 '15 at 18:37
  • But a simple workaround like `$((i+=1))` works in Dash, too. – tripleee Sep 09 '15 at 04:00
  • `$(($i+1))` is the simple workaround I was thinking of. – Josip Rodin Sep 13 '15 at 21:55
2

I didn't particularly like any of the answers.

Here is how I did it.

# Convert the file into an array of strings
lines=(`cat "foo.txt"`)

# Print out the lines via array index
echo "${lines[0]}"
echo "${lines[1]}"
echo "${lines[5]}"
1

In parallel with William Pursell's answer, here is a simple construct which should work even in the original v7 Bourne shell (and thus also places where Bash is not available).

i=0
while read line; do
    i=`expr "$i" + 1`
    case $i in 5) echo "$line"; break;; esac
done <file

Notice also the optimization to break out of the loop when we have obtained the line we were looking for.

Community
  • 1
  • 1
tripleee
  • 175,061
  • 34
  • 275
  • 318
1
#!/bin/bash
for i in {1..50}
do
 line=$(sed "${i}q;d" file.txt)
 echo $line
done
JoKalliauer
  • 1,648
  • 1
  • 13
  • 17
1

Assuming the question was asked for bash, here is the fastest simplest way to do this.

readarray -t a <file ; echo ${a[5-1]}

You may may discard array a when not needed anymore.

Phi
  • 735
  • 7
  • 22
0

Easy with perl! If you want to get line 1, 3 and 5 from a file, say /etc/passwd:

perl -e 'while(<>){if(++$l~~[1,3,5]){print}}' < /etc/passwd
dagelf
  • 1,468
  • 1
  • 14
  • 25
  • `seq 5 | perl -ne 'print if $. ~~ [1, 4, 5]'` but smartmatch is experimental and it's use discouraged – Sorin Nov 21 '19 at 08:47
  • Not a single one of the other solutions are this concise, or allows this much flexibility. (Why does it seem that everything that saves time and makes things easier, is "discouraged" by "smart people", are we all supposed to stare at screens all day?) – dagelf Dec 03 '19 at 10:45
  • Glad to see some solution with perl which is a Swiss army knife for file filtering. I would suggest small enhancement -> `cat /etc/password | perl -ne 'next unless ++$l == 4; print'` if you are interested in one single line. To keep your flexibility without experimental -> `cat /etc/password | perl -ne 'next unless $ref{++$l}; print; BEGIN{ %ref = map{$_=>1} (1,4,5)}` – Xtof Oct 03 '21 at 00:37
0
line=5; prep=`grep -ne ^ file.txt | grep -e ^$line:`; echo "${prep#$line:}"
Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
Oder
  • 11
  • 1
  • 3
    could you describe a little at least why this work to make it clearer to the person who asked the question? – ted Nov 24 '16 at 23:44
  • So, the first grep selects all the lines adding line numbers at their beginnings. Then the second grep selects a specific line by matching the line number at start. And finally the line number is trimmed from the line start in echo. – Oder Nov 24 '16 at 23:58
  • This is both complex and inefficient compared to `sed -n 5p`, which of course can still be optimized to something like `sed -n '5!d;p;q'` – tripleee Dec 15 '19 at 13:07
0

You could use sed command.

If the preferred line number is 5:

sed -n '5p' filename #get the 5th line and prints the value (p stands for print)

If the preferred line number is a range, e.g. 1-5 lines:

sed -n '1,5p' filename #get the 1 to 5th line and prints the values

If need to get 1st and 5th line only, e.g. 1st Line, 5th Line:

sed -n '1p;5p;' filename #get the 1st and 5th line values only
Du-Lacoste
  • 11,530
  • 2
  • 71
  • 51