【问题标题】:diff on strings, not lines字符串上的差异,而不是行
【发布时间】:2012-02-21 02:24:42
【问题描述】:

我觉得我应该能够在睡梦中做到这一点,但是假设我有两个文本文件,每个文本文件都有一列 apache 模块的名称,没有特定的顺序。一个文件有 46 个唯一的(对自己而言)字符串。另一个有 67 行和 67 个 uniq(到文件)字符串。会有很多共同的字符串。

我需要找到-不在较短的第一个文件中但-在-在第二个较长的文件中的 apache 模块的名称。

我想通过搜索和比较字符串来做到这一点。行号、顺序或位置完全不相关。我只想知道需要安装哪些仅在较长文件中列出的模块。

默认情况下,uniq、comm 和 diff 希望按行和行号工作。 我不想并排比较;我只想要一个清单。

【问题讨论】:

  • 你能从所有涉及的文件中提取小样本吗?还有预期的输出?
  • 您是否关心较短文件中的某些项目没有出现在较长文件中,或者这甚至不可能?

标签: bash sorting grep diff uniq


【解决方案1】:

将您的字符串分成几行,对它们进行排序和唯一化,然后使用comm 进行分析。 (见BashFAQ #36)。

举个例子,我假设您想比较两个 Apache 配置文件之间的 LoadModule 指令。

文件1:

...other stuff...
LoadModule foo modules/foo.so
LoadModule bar modules/bar.so
LoadModule baz modules/baz.so
...other stuff...

文件2:

...other stuff...
LoadModule foo modules/foo.so
...other stuff...

所以,要这样做:

comm -2 -3 \
  <(gawk '/LoadModule/ { print $2 }' file1 | sort -u)
  <(gawk '/LoadModule/ { print $2 }' file2 | sort -u)

...将抑制在两个或仅在较短文件中找到的任何行,并为您提供在第三个文件中找到的模块名称,产生以下输出:

bar
baz

对于考虑更多有趣用例的人来说——不幸的是,虽然 GNU 排序的 -z 标志可以处理 NUL 分隔符(以允许对包含换行符的字符串进行比较),但 comm 不能。但是,您可以在支持 NUL 分隔符的 shell 中编写自己的 comm 实现,例如以下示例:

#!/bin/bash
exec 3<"$1" 4<"$2"

IFS='' read -u 4 -d ''; input_two="$REPLY"

while IFS='' read -u 3 -d '' ; do
    input_one="$REPLY"
    while [[ $input_two < $input_one ]] ; do
        IFS='' read -u 4 -d '' || exit 0
        input_two="$REPLY"
    done
    if [[ $input_two = "$input_one" ]] ; then
        printf '%s\0' "$input_two"
    fi
done

【讨论】:

  • 这两个文件只是 apache 模块文件的列表,如下所示: mod_vhost_alias.so mod_mem_cache.so mod_status.so mod_ext_filter.so mod_authz_user.so mod_rewrite.so mod_imagemap.so mod_cgi.so
  • @user189395 如果是这种情况,您可以省略 gawk 位,直接通过sort -u 发送内容。如果文件是空格分隔的,您可以在 sort 之前通过管道将它们通过 tr ' ' '\n' 将其更改为行分隔。
【解决方案2】:

我会像这样运行一个小 bash 脚本(differ.bash):

#!/bin/bash
f1=$1; # longer file
f2=$2; # shorter file

for item in `cat $f1`
do
    match=0
    for other in `cat $f2`
    do
        if [ "$item" == "$other" ]
        then
            match=1
            break
        fi
    done
    if [ $match != 1 ]
    then
        echo $item
    fi
done

exit 0

像这样运行它:

$ ./differ.bash file1 file2

基本上,我只是设置了一个双循环,外循环上的文件较长,内循环上的文件较短。这样,较长列表中的每个项目都会与较短列表中的项目进行比较。这使我们能够在较小的列表中找到所有不匹配的项目。


编辑:我试图用这个更新的脚本来解决查尔斯的第一条评论:

#!/bin/bash
f1=$1; # longer file
f2=$2; # shorter file

while read item
do
    others=( "${others[@]}" "$item" )
done < $f2

while read item
do
    match=0
    for other in $others
    do
        if [ "$item" == "$other" ]
        then
            match=1
            break
        fi
    done
    if [ $match != 1 ]
    then
        echo $item
    fi
done < $f1

exit 0

【讨论】:

  • 使用裸 cat $f1 将文件的内容暴露给字符串拆分(在这种情况下可能需要),还有通配符扩展 - 例如,如果有一个条目在文件f* 中,它将被替换为当前目录中以f 开头的所有文件的名称。可能不是您想要的行为。此外,在内部循环中读取文件而不是预先将其内容存储在数组中是不必要的低效。
  • ...除了具有高常数值的O(n*m) 复杂性之外,这种方法还要求两个列表能够同时放入内存中; comm 一次只读取一行,GNU sort 可以使用临时文件对大于可用 RAM 的输入进行排序和合并。
  • 同意,如果 OP 试图比较每个 1G+ 的文件,那么他可能会遇到一些麻烦。我认为没有必要考虑这一点,因为他的文件包含 46 和 67 项。如果他的比较文件变得非常大,他可能需要找到不同的方法。
  • 我想你想要for other in "${others[@]}" 如果将它用作数组。此外,您可以在非古代版本的 shell 中以 my_array+=( "new_item" ) 的形式附加到数组。
  • ...另外,由于您使用的是 bash,我会考虑 (( match != 1 ))(匹配上下文)或 [[ $match != 1 ]](字符串上下文,但作为没有字符串拆分的 shell 关键字在$match 上;否则如果$match 为空,您可能会收到语法错误)。还有echo "$item",而不是echo $item,以避免变量内容的全局扩展。
猜你喜欢
  • 1970-01-01
  • 2022-12-17
  • 2011-01-08
  • 1970-01-01
  • 2010-10-02
  • 1970-01-01
  • 2013-05-19
  • 2012-01-02
  • 2015-02-07
相关资源
最近更新 更多