使用sed或awk从文件中删除特定行

时间:2013-10-02 01:59:09

标签: linux bash unix

我需要使用bash脚本从文件中删除特定的行号。

我使用-n选项从grep命令获取行号。

我出于各种原因无法使用sed,其中最不重要的是它没有安装在这个脚本需要运行的所有系统上,并且安装它不是一种选择。

awk是不可能的,因为在测试中,在具有不同UNIX / Linux操作系统(RHEL,SunOS,Solaris,Ubuntu等)的不同机器上,它会在每个机器上给出(有时是非常不同的)不同的结果。所以,没有awk。

有问题的文件只是一个平面文本文件,每行一条记录,所以除了删除数字之外,不需要做任何花哨的事情。

如果可能的话,我需要避免做一些事情,比如提取文件的内容,不包括我想要的行,然后覆盖原始文件。

7 个答案:

答案 0 :(得分:6)

由于你有grep,显而易见的事情是:

$ grep -v "line to remove" file.txt > /tmp/tmp
$ mv /tmp/tmp file.txt
$

但听起来你不想使用任何临时文件 - 我认为输入文件很大,这是一个内存和存储空间不足的嵌入式系统。我认为你理想情况下需要一个可以编辑文件的解决方案。我认为dd可能会出现这种情况,但尚未弄明白:(

更新 - 我想出了如何使用dd编辑文件。此外,还需要grepheadcut。如果这些不可用,那么它们可能在大多数情况下可以解决:

#!/bin/bash

# get the line number to remove
rline=$(grep -n "$1" "$2" | head -n1 | cut -d: -f1)
# number of bytes before the line to be removed
hbytes=$(head -n$((rline-1)) "$2" | wc -c)
# number of bytes to remove
rbytes=$(grep "$1" "$2" | wc -c)
# original file size
fsize=$(cat "$2" | wc -c)
# dd will start reading the file after the line to be removed
ddskip=$((hbytes + rbytes))
# dd will start writing at the beginning of the line to be removed
ddseek=$hbytes
# dd will move this many bytes
ddcount=$((fsize - hbytes - rbytes))
# the expected new file size
newsize=$((fsize - rbytes))
# move the bytes with dd.  strace confirms the file is edited in place
dd bs=1 if="$2" skip=$ddskip seek=$ddseek conv=notrunc count=$ddcount of="$2"
# truncate the remainder bytes of the end of the file
dd bs=1 if="$2" skip=$newsize seek=$newsize count=0 of="$2"

如此运行:

$ cat > file.txt
line 1
line two
line 3
$ ./grepremove "tw" file.txt
7+0 records in
7+0 records out
0+0 records in
0+0 records out
$ cat file.txt
line 1
line 3
$ 

可以说dd非常危险的工具。您可以轻松地无意中覆盖文件或整个磁盘。要小心!

答案 1 :(得分:4)

试试。下面的基于此文档的示例从2

中删除行test.txt
ed -s test.txt <<!
2d
w
!

答案 2 :(得分:2)

如果n是您要忽略的行:

{
  head -n $(( n-1 )) file
  tail +$(( n+1 )) file
} > newfile

答案 3 :(得分:2)

你可以在没有grep的情况下使用posix shell builtins来实现,它应该在任何* nix上。

while read LINE || [ "$LINE" ];do
  case "$LINE" in
    *thing_you_are_grepping_for*)continue;;
    *)echo "$LINE";;
  esac
done <infile >outfile

答案 4 :(得分:2)

鉴于dd被认为对于此就地行删除而言太危险,我们需要一些其他方法来对文件系统调用进行相当细粒度的控制。我最初的冲动是用c写一些东西,但尽管可能,我认为这有点过分。相反,值得研究通用脚本(而不是shell脚本)语言,因为这些语言通常具有相当低级别的文件API,这些API以相当简单的方式映射到文件系统调用。我猜这可以使用python,perl,Tcl或许多其他可能的脚本语言来完成。我对Tcl最熟悉,所以我们走了:

#!/bin/sh
# \
exec tclsh "$0" "$@"

package require Tclx

set removeline [lindex $argv 0]
set filename [lindex $argv 1]

set infile [open $filename RDONLY]
for {set lineNumber 1} {$lineNumber < $removeline} {incr lineNumber} {
    if {[eof $infile]} {
        close $infile
        puts "EOF at line $lineNumber"
        exit
    }
    gets $infile line
}
set bytecount [tell $infile]
gets $infile rmline

set outfile [open $filename RDWR]
seek $outfile $bytecount start

while {[gets $infile line] >= 0} {
    puts $outfile $line
}

ftruncate -fileid $outfile [tell $outfile]
close $infile
close $outfile

注意我的特定框我有Tcl 8.4,所以我必须加载Tclx包才能使用ftruncate命令。在Tcl 8.5中,可以使用chan truncate代替。

您可以将要删除的行号和文件名传递给此脚本。

简而言之,脚本执行此操作:

  • 打开文件进行阅读
  • 阅读前n-1行
  • 获取下一行开头的偏移量(第n行)
  • 读取行n
  • 使用新FD打开文件
  • 将写入FD的文件位置移动到第n行开头的偏移量
  • 继续读取读FD中的剩余行并将其写入写FD,直到读取整个读取FD
  • 截断写FD

准确编辑文件。没有使用临时文件。

我很确定这可以用python或perl重写,或者......如果有必要的话。

更新

好的,所以使用与上面的Tcl脚本类似的技术,可以在几乎纯粹的bash中完成就地删除行。但最重要的警告是你需要truncate命令。我确实在我的Ubuntu 12.04 VM上有它,但不是在我的旧的基于Redhat的盒子上。这是脚本:

#!/bin/bash

n=$1
filename=$2
exec 3<> $filename
exec 4<> $filename
linecount=1
bytecount=0
while IFS="" read -r line <&3 ; do
    if [[ $linecount == $n ]]; then
        echo "omitting line $linecount: $line"
    else
        echo "$line" >&4
        ((bytecount += ${#line} + 1))
    fi
    ((linecount++))
done
exec 3>&-
exec 4>&-

truncate -s $bytecount $filename
#### or if you can tolerate dd, just to do the truncate:
# dd of="$filename" bs=1 seek=$bytecount count=0
#### or if you have python
# python -c "open(\"$filename\", \"ab\").truncate($bytecount)"

我希望听到一种更通用的(仅限bash?)方式来完成部分截断并完成此答案。当然截断也可以用dd完成,但我认为已经排除了我之前的答案。

记录this site列出了如何在许多不同语言中进行就地文件截断 - 以防在您的环境中使用这些文件。

答案 5 :(得分:1)

如果您可以指出最明显的Awk脚本在哪个平台上失败的情况,也许我们可以设计一个解决方法。

awk "NR!=$N" infile >outfile

如果当然,只需将$Ngrep一起提供给Awk就可以获得很好的低音效果。这将删除包含第一次出现的foo

的行
awk '/foo/ { if (!p++) next } 1' infile >outfile

答案 6 :(得分:-1)

基于Digital Trauma的回答,我发现了一个只需要grep和echo的改进,但是没有tempfile:

echo $(grep -v PATTERN file.txt) > file.txt

根据文件所包含的行类型以及模式是否需要更复杂的语法,您可以使用双引号包含grep命令:

echo "$(grep -v PATTERN file.txt)" > file.txt

(从crontab中删除时很有用)