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.
8 Answers
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
-
Thanks Panki! However this won't delete line from the file. We need permanent change in file. Commented Apr 17, 2019 at 9:09
-
2Use output redirection to write to a new file, then replace the original one.– PankiCommented Apr 17, 2019 at 9:17
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 beforesed
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 toend
because there was no successful substitution before. Such no-op substitutions/^//
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. SinceD
starts a new cycle, the same line of code will eventually be executed again. This timet
will branch toend
.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) andn=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.
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
ined -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
...
-
I like to add
-s
to the call toed
; quiets the output a bit.– Jeff Schaller ♦Commented Apr 17, 2019 at 15:45 -
-
Is there not redundancy in the first command? IOW,
printf "\$-%d+1d\nwq\n" $n| ed -s file
is legal, no?– B LayerCommented Jun 4, 2019 at 8:04
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
You can combine head
and tail
to achieve this.
If the nth line from the bottom needs to be deleted
head
reads from standard input and reports to standard output total -n lines from the toptail
reads and reports to standard output the bottom n-1 lines from standard input.- 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
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.
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
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.