web-dev-qa-db-ja.com

ファイルの特定のセクションをフィルタリングまたはパイプする

開始タグと終了タグで区切られたいくつかのセクションを含む入力ファイルがあります。次に例を示します。

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

このファイルに変換を適用して、X、Y、Z行がいくつかのコマンド(nlなど)でフィルターされるようにしますが、残りの行は変更されずに通過します。 nl(行数)は行をまたいで状態を累積するため、X、Y、Zの各行に適用されているのは静的変換ではないことに注意してください。 (編集nlは累積状態を必要としないモードで動作できることが指摘されましたが、単純化する例としてnlを使用しています質問。実際には、コマンドはより複雑なカスタムスクリプトです私が本当に探しているのは、標準フィルターを入力ファイルのサブセクションに適用する問題に対する一般的な解決策です

出力は次のようになります。

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D

ファイルには、変換が必要ないくつかのセクションがある場合があります。

pdate 2最初は、セクションが複数ある場合にどうなるかを指定していませんでした。たとえば、

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
 @@inline-code-start
line L
line M
line N
@@inline-code-end

私の期待は、状態が特定のセクション内でのみ維持される必要があることであり、

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D
     1 line L
     2 line M
     3 line N

しかし、問題をセクション全体で状態を維持する必要があると解釈することは有効であり、多くのコンテキストで役立ちます。

更新の終了2

私の最初の考えは、どのセクションにいるかを追跡する単純な状態マシンを構築することです。

#!/usr/bin/bash
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  Elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  Elif [[ $active = true ]]
  then
    # pipe
  echo $line | nl
  else
    # output
    echo $line
  fi
done

私が実行するもの:

cat test-inline-codify | ./inline-codify

nlへの各呼び出しは独立しているため、これは機能しません。したがって、行番号は増加しません。

line A
line B
     1  line X
     1  line Y
     1  line Z
line C
line D

私の次の試みはfifoを使用することでした:

#!/usr/bin/bash
mkfifo myfifo
nl < myfifo &
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  Elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  Elif [[ $active = true ]]
  then
    # pipe
    echo $line > myfifo
  else
    # output
    echo $line
  fi
done
rm myfifo

これは正しい出力を提供しますが、順序が間違っています:

line A
line B
line C
line D
     1  line 1
     2  line 2
     3  line 3

おそらくいくつかのキャッシュが行われています。

私はこれについてすべて間違っていますか?これはかなり一般的な問題のようです。これを解決する単純なパイプラインがあるべきだと私は感じています。

14
James Scriven

1つの可能性は、vimテキストエディターでこれを行うことです。シェルコマンドを介して任意のセクションをパイプできます。

これを行う1つの方法は、:4,6!nlを使用して行番号を指定することです。このexコマンドは、4〜6行目でnlを実行し、入力例で必要なものを実現します。

別のよりインタラクティブな方法は、行選択モード(Shift-V)と矢印キーまたは検索を使用して適切な行を選択し、次に:!nlを使用することです。入力例の完全なコマンドシーケンスは次のようになります。

/@@inline-code-start
jV/@@inline-code-end
k:!nl

これは自動化にはあまり適していません(たとえば、sedを使用した回答の方が適しています)が、1回限りの編集では、20行のシェルスクリプトに頼る必要がないので非常に便利です。

Vi(m)に慣れていない場合は、少なくともこれらの変更後に:wqを使用してファイルを保存できることを知っておく必要があります。

4
marcelm

目的がコードブロック全体を単一のプロセスインスタンスに送信することである場合、コードブロックの最後に到達するまでラインを蓄積し、パイプを遅らせることができます。

#!/bin/bash

acc=""

while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
    acc=""
  Elif [[ $line == @@inline-code-end* ]]
  then
    active=false
    # Act on entire block of code
    echo "${acc:1}" | nl  # Chops off first leading new-line character using ${VAR:1}
  Elif [[ $active = true ]]
  then
    acc=$( printf "%s\n%s" "$acc" "$line" )
  else
    # output
    echo $line
  fi
done

これにより、テストケースを3回繰り返す入力ファイルに対して以下が生成されます。

line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D

コードブロックで何か他のことを行うには(たとえば、逆にしてから番号を付ける)、それを何か他のものにパイプします:echo -E "${acc:1}" | tac | nl。結果:

line A
line B
     1  line Z
     2  line Y
     3  line X
line C
line D

またはワードカウントecho -E "${acc:1}" | wc

line A
line B
      3       6      21
line C
line D
2
Supr

私が考えることができる最も簡単な修正は、nlを使用しないで、自分で行を数えることです:

#!/usr/bin/env bash
while read line
do
    if [[ $line == @@inline-code-start* ]]
    then
        active=true
    Elif [[ $line == @@inline-code-end* ]]
    then
        active=false
    Elif [[ $active = true ]]
    then
        ## Count the line number
        let num++;
        printf "\t%s %s\n" "$num" "$line"
    else
        # output
        printf "%s\n" "$line"
    fi
done

次に、それをファイルに対して実行します。

$ foo.sh < file
line A
line B
    1 line X
    2 line Y
    3 line Z
line C
line D
2
terdon

編集ユーザー指定のフィルターを定義するオプションを追加

#!/usr/bin/Perl -s
use IPC::Open2;
our $p;
$p = "nl" unless $p;    ## default filter

$/ = "\@\@inline-code-end\n";
while(<>) { 
   chomp;
   s/\@\@inline-code-start\n(.*)/pipeit($1,$p)/se;
   print;
}

sub pipeit{my($text,$pipe)=@_;
  open2(my $R, my $W,$pipe) || die("can open2");
  local $/ = undef;
  print $W $text;
  close $W;
  return <$R>;
}

デフォルトでは、フィルターは「nl」です。フィルターを変更するには、ユーザー指定のコマンドでオプション "-p"を使用します。

codify -p="wc" file

または

codify -p="sed -e 's@^@ ║ @; 1s@^@ ╓─\n@; \$s@\$@\n ╙─@'" file

この最後のフィルターは出力します:

line A
line B
 ╓─
 ║ line X
 ║ line Y
 ║ line Z
 ╙─
line C
line D

pdate 1 IPC :: Open2の使用にはスケーリングの問題があります:buffersizeを超えるとブロックされる可能性があります。 (私のマシンでは、64Kが10_000 x "line Y"に対応する場合のパイプのバッファーサイズ)。

より大きなものが必要な場合(10000の「行Y」がさらに必要ですか):

(1)use Forks::Super 'open2';をインストールして使用する

(2)または関数pipeitを次のように置き換えます:

sub pipeit{my($text,$pipe)=@_;
  open(F,">","/tmp/_$$");
  print F $text;
  close F;
  my $out = `$pipe < /tmp/_$$ `;
  unlink "/tmp/_$$";
  return $out;
}
2
JJoao

これはawkの仕事です。

#!/usr/bin/awk -f
$0 == "@@inline-code-start" {pipe = 1; next}
$0 == "@@inline-code-end" {pipe = 0; close("nl"); next}
pipe {print | "nl"}
!pipe {print}

スクリプトが開始マーカーを検出すると、nlへのパイプを開始する必要があることを示します。 pipe変数がtrue(ゼロ以外)の場合、出力はnlコマンドにパイプされます。変数がfalse(未設定またはゼロ)の場合、出力は直接出力されます。パイプされたコマンドは、各コマンド文字列のパイプ構成が最初に検出されたときに分岐されます。同じ文字列を使用した後続のパイプ演算子の評価では、既存のパイプが再利用されます。別の文字列値は別のパイプを作成します。 close関数は、指定されたコマンド文字列のパイプを閉じます。


これは基本的に、名前付きパイプを使用するシェルスクリプトと同じロジックですが、綴りがはるかに簡単で、閉じるロジックは正しく行われます。 nlコマンドを終了してバッファーをフラッシュするには、適切なタイミングでパイプを閉じる必要があります。スクリプトが実際にパイプを閉じるのが早すぎます。最初のecho $line >myfifoの実行が完了するとすぐにパイプが閉じます。ただし、nlコマンドは、次回スクリプトがecho $line >myfifoを実行する前にタイムスライスを取得した場合にのみ、ファイルの終わりを確認します。大量のデータがある場合、またはmyfifoへの書き込み後にsleep 1を追加した場合、nlは最初の行または最初の一連の短い行のみを処理することがわかります、入力の終わりを見たので終了します。

構造を使用して、パイプが不要になるまで開いたままにする必要があります。パイプへの単一の出力リダイレクトが必要です。

nl <myfifo &
exec 3>&1
while IFS= read -r line
do
  if [[ $line == @@inline-code-start* ]]
  then
    exec >myfifo
  Elif [[ $line == @@inline-code-end* ]]
  then
    exec >&3
  else
    printf '%s\n' "$line"
  fi
done

(私はまた、適切な引用符などを追加する機会を得ました—参照 なぜシェルスクリプトが空白やその他の特殊文字を窒息させるのですか?

その場合は、名前付きパイプではなくパイプラインを使用することもできます。

while IFS= read -r line
do
  if [[ $line == @@inline-code-start* ]]
  then
    while IFS= read -r line && [[ $line != @@inline-code-end* ]] do
      printf '%s\n' "$line"
    done | nl
  else
    printf '%s\n' "$line"
  fi
done

さて、最初に。 ファイルのセクションの行に番号を付ける方法を探しているのではないことを理解しています。フィルターの実際の例(nl以外)を示していないので、

tr "[[:lower:]]" "[[:upper:]]"

つまり、テキストをすべて大文字に変換します。ので、の入力

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

あなたはの出力が欲しい

line A
line B
LINE X
LINE Y
LINE Z
line C
line D

ソリューションの最初の近似は次のとおりです。

#!/bin/sh
> file0
> file1
active=0
nl -ba "$@" | while IFS= read -r line
do
        case "$line" in
            ([\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9]"        @@inline-code-start")
                active=1
                ;;
            ([\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9][\ 0-9]"        @@inline-code-end")
                active=0
                ;;
            (*)
                printf "%s\n" "$line" >> file$active
        esac
done
(cat file0; tr "[[:lower:]]" "[[:upper:]]" < file1) | sort | sed 's/^[ 0-9]\{6\}        //'

ここで、@@文字列の前、および最終行の終わり近くのスペースはタブです。私はnlを自分の目的で使用していることに注意してください。 (もちろん、私はyour問題を解決するためにそれを行っていますが、行番号付きの出力を与えることはしていません。)

これにより、入力の行に番号が付けられるため、セクションマーカーで入力を分解し、後で再び組み合わせる方法を知ることができます。ループの本体は、セクションマーカーに行番号が付いているという事実を考慮して、最初の試行に基づいています。入力を2つのファイルに分割します:file0(非アクティブ;セクションではありません)とfile1(アクティブ;inセクション)。これは、上記の入力では次のようになります。

file0:
     1  line A
     2  line B
     8  line C
     9  line D

file1:
     4  line X
     5  line Y
     6  line Z

次に、file1(これはallセクション内の行を連結したもの)を大文字のフィルターで実行します。それをフィルタリングされていないセクション外の線と組み合わせる。並べ替え、元の順序に戻す。そして、行番号を取り除きます。これにより、私の回答の上部に表示される出力が生成されます。

これは、フィルターが行番号をそのままにすることを前提としています。そうでない場合(たとえば、行の先頭で文字を挿入または削除する場合)、この一般的なアプローチは引き続き使用できますが、少し複雑なコーディングが必要になると思います。

0
Scott

Sedを使用して境界のない行のチャンクを出力し、区切られた行のチャンクをフィルタープログラムにフィードするシェルスクリプト:

#!/bin/bash

usage(){
    echo "  usage: $0 <input file>"
}

# Check input file
if [ ! -f "$1" ]; then
    usage
    exit 1
fi

# Program to use for filtering
# e.g. FILTER='tr X -'
FILTER='./filter.sh'

# Generate arrays with starting/ending line numbers of demarcators
startposs=($(grep -n '^@@inline-code-start$' "$1" | cut -d: -f1))
endposs=($(grep -n '^@@inline-code-end$' "$1" | cut -d: -f1))

nums=${#startposs[*]}
nume=${#endposs[*]}

# Verify both line number arrays have the same number of elements
if (($nums != $nume)); then
    echo "Tag mismatch"
    exit 2
fi

lastline=1
i=0
while ((i < nums)); do
    # Exclude lines with code demarcators
    sprev=$((${startposs[$i]} - 1))
    snext=$((${startposs[$i]} + 1))
    eprev=$((${endposs[$i]} - 1))

    # Don't run this bit if the first demarcator is on the first line
    if ((sprev > 1)); then
        # Output lines leading up to start demarcator
        sed -n "${lastline},${sprev} p" "$1"
    fi

    # Filter lines between demarcators
    sed -n "${snext},${eprev} p" "$1" | $FILTER

    lastline=$((${endposs[$i]} + 1))
    let i++
done

# Output lines (if any) following last demarcator
sed -n "${lastline},$ p" "$1"

このスクリプトをdetagger.shという名前のファイルに書き込み、次のように使用しました:./detagger.sh infile.txt。問題のフィルタリング機能を模倣するために、別のfilter.shファイルを作成しました。

#!/bin/bash
awk '{ print "\t" NR " " $0}'

ただし、フィルタリング操作はコードで変更できます。

私はgeneric solutionのアイデアに従ってこれを試みたので、行の番号付けなどの操作で追加の/内部のカウントが必要なくなりました。スクリプトは基本的なチェックを行って、境界タグがペアになっていて、ネストされたタグを正常に処理しないことを確認します。

0
Pooping