たくさんのファイルがあり、特定の文字列で始まる連続した行が含まれているファイルを見つけたい。
たとえば、次のファイルの場合:
Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Cyyyyyyyyy
Czzzzzzzzz
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd
Ceeeeee
「C」で始まる行が複数あるので、このファイルをコマンドで見つけたいと思います。
たとえば、次のファイルの場合:
Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd
「C」で始まる行は常に1つあります。このファイルは必要ありません。 grep
またはsed
を使用することを考えましたが、正確な方法がわかりません。多分正規表現^C.*$^C
またはそのようなもの。何か案が ?
pcregrep
の場合:
pcregrep -rMl '^C.*\nC' .
POSIXly:
find . -type f -exec awk '
FNR==1 {last=0; printed=0; next}
printed {next}
/^C/ {if (last) {print FILENAME; printed=1; nextfile} else last=1; next}
{last=0}' {} +
(ただし、awk
をサポートしていないnextfile
実装ですべてのファイルを完全に読み取ることを意味します)。
GNU grep
のバージョン2.5.4まで:
grep -rlP '^C.*\nC' .
が表示されますが動作しますが、これは偶然によるものであり、動作が保証されていません。
2.6で修正される前( this commit )によって、GNU grep
は、使用しているpcre検索機能が全体的に一致することを見落としていたgrep
によって現在処理されているバッファは、あらゆる種類の驚くべき動作を引き起こします。
grep -P 'a\s*b'
以下を含むファイルに一致します:
bla
bla
これは一致します:
printf '1\n2\n' | grep -P '1\n2'
しかしこれは:
(printf '1\n'; sleep 1; printf '2\n') | grep -P '1\n2'
または:
(yes | head -c 32766; printf '1\n2\n') > file; grep -P '1\n2' file
しない(1\n2\n
は、grep
によって処理された2つのバッファにまたがっています。
しかし、その動作は文書化されていました:
15-どうすれば行を越えて照合できますか?
基本的に行ベースであるため、標準のgrepはこれを実行できません。したがって、「[:space:]」文字クラスを使用するだけでは、期待した方法で改行が一致しません。ただし、grepがPerlパターンを有効にしてコンパイルされている場合、Perlの「s」修飾子(「。」を改行に一致させる)を使用できます。
printf 'foo\nbar\n' | grep -P '(?s)foo.*?bar'
2.6で修正された後、ドキュメントは修正されませんでした(以前に報告した there )。
awk
の場合:
awk '{if (p ~ /^C/ && $1 ~ /^C/) print; p=$1}' afile.txt
C
で始まる連続した行がある場合、これはファイルの内容を出力します。表現 (p ~ /^C/ && $1 ~ /^C/)
はファイル内の連続する行を調べ、両方の最初の文字がC
と一致する場合にtrueと評価します。その場合、行が印刷されます。
このようなパターンを持つすべてのファイルを見つけるには、find
コマンドを使用して上記のawkを実行できます。
find /your/path -type f -exec awk '{if (p ~ /^C/ && $1 ~ /^C/) {print FILENAME; exit;} p=$1}' {} \;
このコマンドでは、find
+ exec
は各ファイルを通過し、各ファイルに対して同様のawk
フィルタリングを実行し、FILENAME
を介してその名前を出力します。 awk式はtrueと評価されました。複数の一致がある単一のファイルでFILENAME
を複数回印刷しないようにするために、exit
ステートメントが使用されます(@terdonに感謝)。
GNU sed
を使用したさらに別のオプション:
単一ファイルの場合:
sed -n -- '/^C/{n;/^C/q 1}' "$file" || printf '%s\n' "$file"
(ただし、読み取れないファイルも報告されます)。
find
の場合:
find . -type f ! -exec sed -n '/^C/{n;/^C/q 1}' {} \; -print
読み取れないファイルが印刷される問題は、次のように書くことで回避できます。
find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
ファイルがメモリに読み込まれるほど小さいと仮定します。
Perl -000ne 'print "$ARGV\n" if /^C[^\n]*\nC/sm' *
説明:
000
:\n\n
をレコードセパレータとして設定します。これにより、段落モードがオンになり、(連続する改行で区切られた)段落が1行として扱われます。-ne
:-e
の引数として指定されたスクリプトを、入力ファイルの各行に適用します。$ARGV
:現在処理中のファイルです/^C[^\n]*\nC/
:行の先頭でC
に一致(これが機能する理由については、以下のsm
修飾子の説明を参照)の後に0個以上の非改行文字、a改行、次に別のC。つまり、C
で始まる連続する行を見つけます。 * //sm
:これらの一致修飾子は([ここに記載]のとおり):
m:文字列を複数行として扱います。つまり、 "^"と "$"を、文字列の左端と右端のみの行の最初または最後と一致するようから、文字列内の任意の場所と一致するように変更します。
s:文字列を1行として扱います。つまり、「。」を変更します。通常は一致しない改行を含め、すべての文字に一致します。
次のような醜いこともできます:
for f in *; do Perl -pe 's/\n/%%/' "$f" | grep -q 'C[^%]*%%C' && echo "$f"; done
ここで、Perl
コードは改行を%%
に置き換えます。したがって、入力ファイルに%%
がないと想定します(bigifもちろん)、grep
はC
で始まる連続する行に一致します。
( set -- *files ; for f ; do (
set -- $(printf %c\ `cat <$f`)
while [ $# -ge 1 ] ;do [ -z "${1#"$2"}" ] && {
echo "$f"; break ; } || shift
done ) ; done )
まず、テストベースを作成します。
abc="a b c d e f g h i j k l m n o p q r s t u v w x y z"
for l in $abc ; do { i=$((i+1)) h= c= ;
[ $((i%3)) -eq 0 ] && c="$l" h="${abc%"$l"*}"
line="$(printf '%s ' $h $c ${abc#"$h"})"
printf "%s$(printf %s $line)\n" $line >|/tmp/file${i}
} ; done
上記は/tmp
という名前の26個のファイルを作成しますfile1-26
。各ファイルには、文字a-z
で始まり、残りが続く27行または28行がありますアルファベットの。 3番目のファイルごとに、最初の文字が重複する2つの連続した行が含まれています。
cat /tmp/file12
...
aabcdefghijkllmnopqrstuvwxyz
babcdefghijkllmnopqrstuvwxyz
cabcdefghijkllmnopqrstuvwxyz
...
kabcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
mabcdefghijkllmnopqrstuvwxyz
...
そして私が変わるとき:
set -- *files
に:
set -- /tmp/file[0-9]*
わかった...
/tmp/file12
/tmp/file15
/tmp/file18
/tmp/file21
/tmp/file24
/tmp/file3
/tmp/file6
/tmp/file9
つまり、簡単に言うと、ソリューションは次のように機能します。
set
sすべてのファイルに対するサブシェルの位置、およびそれぞれの
set
sループする各ファイルの各行の最初の文字に対するネストされたサブシェルの位置。
[ tests ]
if$1
否定$2
一致を示し、そうであれば
echoes
ファイル名次にbreak
s現在のループ反復else
shift
s再試行する次の1文字の位置に
このスクリプトは、grep
およびcut
を使用して一致する行の行番号を取得し、2つの連続する番号があるかどうかを確認します。ファイルは、スクリプトの最初の引数として渡された有効なファイル名と見なされます。
#!/bin/bash
checkfile () {
echo checking $1
grep -n -E "^C.*$" $1 | cut -d: -f1 | while read linenum
do
: $[ ++PRV ]
if [ $linenum == $PRV ]; then return 1; fi
PRV=$linenum
done
return 0
}
PRV="-1"
checkfile $1
if [ $? == 0 ]; then
echo Consecutive matching lines found in file $1
fi