8

There are unknown number of lines in a file. How to delete nth line (when counted from the bottom) with one-liner command (you may use more than one if it is necessary) on Unix platform.

0

8 Answers 8

17

To remove for example the 4th line from the bottom using sed:

tac input | sed '4d' | tac

To overwrite the input file:

tmpfile=$(mktemp)
tac input | sed '4d' | tac > "$tmpfile" && mv "$tmpfile" input
2
  • Thanks Panki! However this won't delete line from the file. We need permanent change in file. Commented Apr 17, 2019 at 9:09
  • 2
    Use output redirection to write to a new file, then replace the original one.
    – Panki
    Commented Apr 17, 2019 at 9:17
5

Pure sed:

  • If n is 1:

    sed '$ d'
    

    This is simple: if it's the last line, delete the pattern space, so it's not printed.

  • If n is greater than 1 (and available as $n):

    sed "
       : start
       1,$((n-1)) { N; b start }
       $ { t end; s/^//; D }
       N
       P
       D
       : end
     "
    

    Note $((n-1)) is expanded by the shell before sed starts.

    This fragment

    : start
    1,$((n-1)) { N; b start }
    

    stores n-1 lines in the pattern space. If sed reaches the end of the input stream during this loop, the pattern space will be printed automatically (there is no n-th line from the end, no line will be deleted).

    Suppose there is more input. Then, before we get to the last line, this fragment is iterated:

    N   # read the next line of input and append it to the pattern space
    P   # print the first line from the pattern space
    D   # delete the first line from the pattern space and start a new cycle
    

    This way the pattern space is our buffer which makes the output several lines "late" according to the input. N from this fragment is able to read the last line of the input as well.

    After the last line is read, this gets executed:

    $ { t end; s/^//; D }
    

    When this code is executed for the first time, t doesn't branch to end because there was no successful substitution before. Such no-op substitution s/^// is then performed and the first line from the pattern space is deleted (D) without being printed. This is exactly the line you want to be deleted. Since D starts a new cycle, the same line of code will eventually be executed again. This time t will branch to end.

    When sed reaches the end of the script, the pattern space is printed automatically. This way all remaining lines get printed.

    The command will generate the same output for n=2 (valid) and n=1 (invalid). I tried to find a single solution that works regardless of n. I failed, hence the special case when your n is 1.

3

if $n hold number of line to delete

to delete a single line use

printf "\$-%d+1,\$-%d+1d\nwq\n" $n $n| ed -s file

to delete n last lines

printf "\$-%d,\$d\nwq\n" $n | ed -s file

where

  • \$%d,\$d tell ed to delete n last lines (printf will insert n)
  • wq write and quit
  • -s in ed -s will keep ed silent.

  • note that no provision is made to check you have enough line to delete.

sadly range from end can't be specified in sed ...

3
  • I like to add -s to the call to ed; quiets the output a bit.
    – Jeff Schaller
    Commented Apr 17, 2019 at 15:45
  • @JeffSchaller done
    – Archemar
    Commented Apr 17, 2019 at 18:36
  • Is there not redundancy in the first command? IOW, printf "\$-%d+1d\nwq\n" $n| ed -s file is legal, no?
    – B Layer
    Commented Jun 4, 2019 at 8:04
2

This is tagged with sed and awk but the question doesn't mention these as being required for the solution. Here's a Perl filter which removes the 4th-from-last line and prints the result. The output can written to a tmp file and then used to replace the original.

 perl -e '@L = <STDIN>; splice(@L,-4,1); print @L' ./lines.txt
1

You can combine head and tail to achieve this.

If the nth line from the bottom needs to be deleted

  1. head reads from standard input and reports to standard output total -n lines from the top
  2. tail reads and reports to standard output the bottom n-1 lines from standard input.
  3. Taken as a whole, the lines are thus reported to standard output in order, with the *nth` line from the end being skipped

This solution depends on head leaving the file offset of the open file description just after the last reported line - I believe GNU head does indeed do this when standard input is redirected from a file

n=200; tmpfile=$(mktemp) && { head -n -$n; tail -n -$((n-1)); }<file >"$tmpfile" \
    && mv -- "$tmpfile" file
1

Neither standard sed nor awk support editing in place.

For that, you better use ed(1) or ex(1):

printf '$-%dd\nw\n' 1 | ed -s your_file

Or with here-doc

ed -s <<'EOT' your_file
$-1d
w
EOT

With advanced shells like bash, zsh orksh93 you can use the $'...' syntax and here-strings:

ed -s <<<$'$-1d\nw' your_file 

Notice that the $ address means the last line from the file; so the index 1 there is 0-based; for the 1st line from the end replace the 1 with 0 ($-0), for the 3nd with 2 ($-2), etc.

Putting it in a function:

del_nth_line_from_end(){ printf '$-%dd\nw\n' "$(($2-1))" | ed -s "$1"; }

Instead of ed -s you can use ex -s or vim -es everywhere.

0

sed cannot calculate nth row from bottom by itself, so we need to that before, e.g. using awk:

Delete 4th row from bottom:

delrow=$(awk -v n=4 'END { print NR-n+1 }' file)
sed -i "${delrow}d" file
0

An one-line, in-place solution:

With gawk, this will delete the 42nd line from the bottom:

gawk -i inplace 'i==0 {if(FNR>t){t=FNR}else{i=1}} i==1 && FNR!=t-42 {print}' input input

The 42 can be replaced with any number. If 0 is used then the last line is deleted.

Notice that the input file is specified twice. This forces two iterations of the file with gawk. In the first iteration (i==0) the total number of lines (t) is established. In the second iteration, the nth line from the end is not output.

The file is modified in place by using the -i option.

You must log in to answer this question.

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