【问题标题】:Remove a specific line from a file WITHOUT using sed or awk不使用 sed 或 awk 从文件中删除特定行
【发布时间】:2013-10-08 08:41:15
【问题描述】:

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

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

我不能使用 sed 的原因有很多,其中最起码的原因是它没有安装在该脚本需要运行的所有系统上,并且安装它不是一个选项。

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

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

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

【问题讨论】:

  • ed 或红色可能会给您想要的:unix.stackexchange.com/questions/58027/…
  • 你能在这篇文章中包含你的 awk 脚本吗?我对您的断言感到非常惊讶(也许 sun 4 的(旧)awk(不是 nawk)除外)。祝你好运。
  • 我没有最初使用的 awk 脚本。这个指令来自比我赚更多钱的人......
  • 查看脚本部分原作者的注释,实际上是我们用作 NIS 服务器的几台 Sun 机器出现了问题。它没有说明导致问题的任何版本。

标签: linux bash unix


【解决方案1】:

既然你有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 是一个非常危险的工具。您可以轻松地无意中覆盖文件或整个磁盘。要非常小心!

【讨论】:

  • 无意冒犯,但本着使用正确的工具完成正确的工作的精神,我真的希望 OP 不会使用 dd 作为解决方案。
  • 没有冒犯。我同意dd 是一个非常危险的工具,应该非常仔细地考虑它的使用。话虽如此,它是一个非常通用的工具,我认为迄今为止唯一一个可以用来正确回答 OPs 问题的工具 - 即从文件中删除该行,没有任何临时文件。
  • 创建临时文件的问题是该文件非常大(在某些系统上高达 1.9 GB),并且它所在的目录实际上是从共享它的 NFS 服务器导出的所有需要使用该文件的机器。该 NFS 服务器还对导出的目录实施配额。如果我尝试将其 cp 到与临时文件相同的目录,那么它很有可能会超过几台机器上的配额。如果它首先尝试将其 cp 到本地机器,这将意味着大量的网络 I/O。是的,我知道设置/拓扑非常复杂。不,不是我设计的……
  • 也就是说,我认为 dd 对于这种情况来说有点太危险了。很棒的概念!
  • "可以说 dd 是一个非常危险的工具。您很容易无意中覆盖文件或整个磁盘。"我的意思是,您可以使用普通的旧 shell 重定向轻松地做到这一点。我认为dd 删除磁盘的声誉来自于我们赋予它 root 权限并将其指向磁盘的偏好。
【解决方案2】:

试试。下面基于 here-document 的示例从 test.txt 中删除行 2

ed -s test.txt <<!
2d
w
!

【讨论】:

  • 我对 Sun ed 的体验是它无法读取非常大的文件而不会中断。使用vis ex 模式的工作方式大致相同,并且仅限于ex 写入其临时文件的可用空间,该文件可配置为:tmp=/path/to/tmpdir(或类似的,请参阅您的vi 文档)。
  • '当然,有人想知道为什么 OP 不只是使用 ed 搜索实际模式,而不是使用 grep -n 获取行号,将其从 grep 中解析出来输出,用它组成ed输入,并将其传递给ed。
  • 当我在strace 下运行它时,我看到创建了一个临时文件:open("/tmp/ed.kNTc8I", O_RDWR|O_CREAT|O_EXCL, 0600) = 3。我发布的dd 解决方案并非如此。
  • @DigitalTrauma,临时文件的大小与原始文件相同吗?
  • @1_CR - 是的。至少我以 1.2MB 的文件交互地运行 ed strace ed -s ed.txt 并在 ed 启动后立即看到一个类似大小的文件出现在 /tmp 中:-rw------- 1 user user 1228800 Oct 2 10:16 /tmp/ed.v0fHPp。一旦 ed 退出,文件就会消失。
【解决方案3】:

如果n 是您要省略的行:

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

【讨论】:

    【解决方案4】:

    你可以在没有 grep 的情况下使用 posix shell 内置函数来做到这一点,它应该在任何 *nix 上。

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

    【讨论】:

      【解决方案5】:

      鉴于dd 被认为对于这种就地行删除来说太危险了,我们需要一些其他方法来对文件系统调用进行相当细粒度的控制。我最初的冲动是用 c 写一些东西,但尽管可能,我认为这有点矫枉过正。相反,值得关注常见的脚本(不是 shell 脚本)语言,因为这些语言通常具有相当低级的文件 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 列出了如何在许多不同的语言中进行就地文件截断 - 以防万一这些语言可以在您的环境中使用。

      【讨论】:

        【解决方案6】:

        如果您可以指出在哪种情况下最明显的 Awk 脚本在哪些平台上对您不利,也许我们可以设计一种解决方法。

        awk "NR!=$N" infile >outfile
        

        当然,使用grep 获取$N 只是为了将其提供给Awk 是非常糟糕的。这将删除包含foo 第一次出现的行:

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

        【讨论】:

        • 甚至:awk '!/foo/||p++' infile &gt; outfile。但这不是到位的,fwiw。
        【解决方案7】:

        根据 Digital Trauma 的回答,我发现了一个改进,只需要 grep 和 echo,但不需要 tempfile:

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

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

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

        (在从 crontab 中删除时很有用)

        【讨论】:

        • 命令替换会将整个grep 输出放入内存(如果可用),然后将整个输出粘贴到echo 命令行。对于大于正常命令行长度限制的输入文件,这将中断,该限制不太可能大于几兆字节 - 对于 OP 来说还不够。更不用说 echo 如何以微妙的方式改变输出 - printf 会更可靠
        猜你喜欢
        • 1970-01-01
        • 2023-03-05
        • 1970-01-01
        • 2018-08-25
        • 2011-12-21
        • 2016-02-04
        • 2023-01-26
        • 1970-01-01
        • 2021-03-22
        相关资源
        最近更新 更多