改行文字を含まない入力ストリームに正規表現変換を適用するために「sed」または他の手段を使用できますか?

改行文字を含まない入力ストリームに正規表現変換を適用するために「sed」または他の手段を使用できますか?

sed式を含むシェルパイプラインがあります。

source | sed -e 's:xxxxx:yyyyy:g' | sink

動作しますが、sed行全体に適用されるという点で潜在的な欠陥があります。これはsink、改行文字が送信されるまで何も表示されないことを意味します。sourceソースが改行を送信しない場合、問題が発生します。

シェルはbashですが関係ないことを願っています。文字列の合計は正規表現にするxxxxxことができ、キャプチャグループを使用していくつかのコンテンツを 。yyyyyxy

sed改行文字を含まない入力ストリームに正規表現変換を使用または適用することは可能ですか?

Rubyでフィルタを書いてこの問題を解決しましたが、コードを書くのではなく既存のツールを使用できるかどうか疑問に思いました。

答え1

実際に考えると、優先的に影響を与えることができるのはみんな可能:

source |
tr '\n<' '<\n'  |
paste -sd\\n - -|
sed  -e'/^[0-9]\{1,\}>/!{$!H;1h;$!d'\
     -e\} -e'x;y/\n</<\n/;s//<&/'   \
     -ew\ /dev/fd/1 |
filter ... | sink

これにより、まずインストリームコンテンツが中断されます。連合国<条件付きで for\nと after の両方を置き換えるホームフレーズ適切に交換してください。言及した区切り文字は次のとおりです。これは必須です。いいえ単一文字(改行文字で)したがって、単純な翻訳だけでは編集内容を保証するには不十分です。最初フローを定義します。つまり、言及した編集内容が必要な場合があります。たとえば、適用されると理解されるグループのキャプチャやその他の状況に合った一致があります。記録- エンドポイントを確認するまでは安定して完了できません。


バッファリングされていない


sed正規表現一致入力が初めて表示される場合にのみバッファリング<[0-9]+>まずすべて翻訳<ewlines\nまたはその逆にして、入力を前のスペースに1行ずつ積み重ねますsedH^[0-9]\{1,\}>一致します。

ただし、チャンクバッファ出力は4kbチャンク以上のパイプに書き込むときに行われますtrpaste

この問題を処理する2つのバージョンもあります。

sol1(){
    {   cat; printf '\n\4\n'; } |
    {   dd obs=1 cbs=512 conv=sync,unblock \
        <"$(pts;stty eol \";cat <&3 >&4&)" 
    }   3<&0 <&- <>/dev/ptmx 2>/dev/null 4>&0 |
    sed  -ne'/<[0-9]\{1,\}>/!{H;$!d' -e\} \
          -e'x;s/\n//g;w /dev/fd/1'
}

これにより、すべての入力がptyにプッシュされ、ddそれから読み取りが設定されます。 fdのロックptsを解除して割り当てるには、他の質問の小さなCプログラムを使用してくださいdd。上記の場合、区切りはカーネルによって行われます。 pty 行規則は次"のように構成されます。stty eolchar - 出力から削除されません。eofcharはそうです。ただし、ptyバッファをdd各発生にプッシュし、それを満たします。read()ddすべてread()Sまず、出力バッファの末尾を512文字のスペースで埋め、次のすべての末尾のスペースを単一の改行文字に圧縮します。

以下は、最後の行がブロックされる問題を解決する修正版です。

sol1_5(){
    {   cat; printf '\n\4\n'; } |
    {   dd ibs=16k obs=2 cbs=4k conv=sync,unblock <"$(pts
        stty raw isig quit \^- susp \^- min 1 time 2
        cat  <&3 >&4&)" 
    }   3<&0 <&- <>/dev/ptmx 2>/dev/null 4>&0 |
    sed -ne's/<[0-9]\{1,\}>/\n&/g;/./w /dev/fd/1'
}

trバッファリングを解放し、まったく異なる別のバージョンは次のとおりですpaste

sol2(){
    stdbuf -o0 tr '\n<' '<\n'  |
    stdbuf -o0 paste -sd\\n - -|
    sed  -ue'/^[0-9]\{1,\}>/!{$!H;1h;$!d'\
         -e\} -e'x;y/\n</<\n/;s//<&/'
}

サンプルデータを使用して両方の方法をテストした。

for sol in 1 2
do  printf '<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"' |
   cat - /dev/tty | "sol$sol" | cat

どちらの場合も、最初の3行はすぐに印刷されますが、4行目はバッファに残ります。sedバッファは次の行の先頭を見つけるまで印刷されないため、EOFまで入力の後に1行を保持します。緊急CTRL+D印刷されました。


<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"
<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"
<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"
<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"

しかし、sol1_5まったく異なるアプローチを使用します。入力を分離するために文字コンテキストに依存するのではなく、各write()4k以下のバイトが少なくとも1つの完全なコンテキストを表す必要があると思うので、適切な改行だと思うものを各バイトに追加し、出力をすぐにフラッシュします。

仕組みは設定することです。stty minそしてtimeddptyの値です。設定するとmin > 0 そして time > 0非標準端末装置では、端末は少なくとも受信されるまで読み取りをブロックします。min文字を入力してから次までブロックし続けます。time10分の1秒が過ぎました。そのようにしてすべてに頼ることができればwrite()端末にはバイトが多すぎ、完了するのに時間がかかりすぎます。個人的にログ書き込みに4kと0.2秒がかなり公正な仮定だと思います。その後、入力を読み取り、出力をフラッシュできます。同期的に

したがって、sol1_54行すべてがすぐに印刷されます。


sedスクリプト


これは実際には非常に簡単な方法であり、複数の文字区切り文字sed(デフォルトでは単一文字のレコードのみを区切る)改行を処理するためにかなり一般的に調整できます。

  1. 区切り文字パターンで最初に表示されるすべての文字を改行文字に変換し、すべての改行文字をその文字に変換します。

    • 以下の複雑さの一部:ストリームの末尾に改行文字があることを確認してください。

    • tr '\n<' '<\n' | paste -sd\\n - -

  2. 区切り文字パターンの残りの部分に新しい改行で区切られた入力を取得します。ただし、行の先頭に発生した場合にのみ該当します。

    • これは簡単であるだけでなく、非常に効率的です。入力行の最初の数文字だけを確認してください。sed作業はほとんどまたはまったく必要ありません。

    • /^[0-9]\{1,\}>/

  3. H一致しない行のコピーを古いスペースに追加して!削除dします。ただし、一致しない行の場合は、x編集および保存バッファを変更して、現在のパターンスペースが完全に区切られた最後のレコードのすべての内容になるようにします。予約済みスペースには、最初に区切られたシーケンスの一部のみが含まれます。

    • 最も複雑なのは、最初の入力ラインと最後の入力ラインに注意を払う必要があることです。ここでの複雑さはsed基本的な効率に由来する。実際には、バッファごとに1つのレコードを処理できます。

    • 何の理由もなく最初の行に追加の行を挿入したくないので、\nこの場合はh古いスペースを追加する代わりに上書きする必要があります。H

    • 皆さんは! いいえ H空または保持バッファがないため、最後の行を削除またはd削除してください。$これ以上スキャンする入力はありませんが、最後に保存した記録を処理する必要があります。

    • /.../!{$!H;1h;$!d;};x

  4. s///これで、完全に区切られたコンテキストを復元するために高価な代替正規表現を適用するのではなく、独自の音訳機能を使用して、保存されているすべての挿入されたewline文字を区切り文字の最初の文字に一度に効率的に置き換えることができます。sedy///\n

    • y/\n</<\n/
  5. <最後に、パターン空間の先頭に新しい行を挿入するだけです。\n挿入する必要がある行は、印刷時に最後のバッファサイクルの最後にすでに追加されているためです。

    • これを行う最も効率的な方法は、//入力行でテストしたのと同じ正規表現を再利用することです。これはsedあなたがする必要があることから逃れるかもしれません。regcomp()単一の正規表現をコンパイルして繰り返しregexec()全体の流れを安定して描写するために、同じ自動装置が繰り返し実行される。

    • s//<&/

これで、この出力ストリームを通常の行で区切られたテキストファイルとして\n扱うことができます。

テスト

printf '%s\n' the string \
              "<9>more $(printf %050s|tr ' ' '<') string" \
              and \<9\> "<more<string and <9> more<string" |
tr '<\n' '\n<'   |
paste -sd\\n - - |
sed  -e'/^[0-9]\{1,\}>/!{$!H;1h;$!d' \
     -e\} -e'x;y/\n</<\n/;s//<&/'

the
string

<9>more <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< string
and

<9>
<more<string and 
<9> more<string

これで、文字列に編集内容を適用することが目標である場合は、次のように説明できます。^<[0-9]+>(^(<[0-9]+>))*まあ、この時点ではおそらく2番目のフィルタは必要ありません。なぜなら、それはsed小さなスクリプトの終わりに印刷される前にパターンスペースが表すのとまったく同じです(\newlinesなど)。

前の例の修正バージョンを再利用して...

文字列>データ

printf '%s\n' the string \
              "<1>more $(printf %050s|tr ' ' '<') string" \
              and \<2\> "<more<string and <3> more<string" |
tr '<\n' '\n<'   |
paste -sd\\n - - |
sed  -e'/^[0-9]\{1,\}>/!{$!H;1h;$!d' \
     -e\} -e'x;y/\n</<\n/;s//<&/'  \
     -e'/^<[2-9][0-9]*>/s/string/data/g'

the
string

<1>more <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< string
and

<2>
<more<data and 
<3> more<data

答え2

プログラムが端末に書き込むと、バッファはすべての改行文字でフラッシュされますが、プログラムを使用できますバッファリング解除 (一部のディストリビューションでは、このコマンドがstdbufであることに注意してください。)

これを試してください。

unbuffer source | sed -e 's:xxxxx:yyyyy:g' | sink

答え3

少なくとも、GNU sedは最終改行なしで入力を処理できます(最後の不完全な行が渡されると、最終改行なしで出力が生成されます)。 Unixのテキストファイルは定義上新しい行で終わる必要があり(空でない場合)、sedはテキストユーティリティであるため、テキスト以外の入力に対するこれらの寛大さは保証されません。

sedはラインを変換するように設計されているため、ほとんどの実装では、変換を適用する前に、特にその入力ラインに対応する出力を生成する前に、入力ライン全体をメモリに読み込むことが期待されます。

sedを使用してこの入力を簡単に処理するには、パターンと一致しないか、代替テキストとして生成されていますが、入力で頻繁に発生する文字を選択しxxxxxてくださいyyyyy。 sedを呼び出して改行文字に変換するか、その逆に変換します。

 source | tr ':\n' '\n:' | sed -e 's:foo:bar:g' | tr ':\n' '\n:' | sink

選択できる良いキャラクターがなければ、sedはおそらく役に立ちません、Rubyは合理的な選択です。

答え4

この問題を解決するために、Rubyで小さなスクリプトを実装しました。使用方法は次のとおりです。

source | myscript.rb | sink

これがソースです

$stdout.sync                                   # no outbound buffering by Ruby
buf=''
$stdin.each_char do |c|
  if buf.length>0 || c=='<'                    # buffering starts when '<' received
    buf << c                                   #        and continues until flushed
    buf.gsub!(/(<\d+>)/,"\n\\1") if (c == '>') # regex transform matching buffer
    unless (buf =~ /<\d*$/)                    # flush buffer when regex fails
      STDOUT << buf
      buf.replace ''                           # empty buffer stops buffering
    end
  else
    STDOUT << c;                               # unbuffered output
  end
  $stdout.flush                                # no buffering, please!
end

Rubyの専門家はこの問題を改善するかもしれませんが、ここに問題を解決した迅速な「汚いハッキング」があります。

デフォルトでは、一度に1文字ずつstdinを読み、一致する最初の文字があることを確認します。つまり、<文字が見つからない場合はすぐにstdinに書き込みます。一致する場合は、バッファに書き込んでから、バッファの内容を正規表現で有効な区切り文字(後<にゼロ以上の数字が続く)が一致しない場合を除き、バッファをフラッシュしてバッファリングを停止します。バッファリング時に a を取得すると、>正規表現で変換を実行します。

修正する

上記のスクリプトは機能しますが、改行文字を待っている場合、ダウンストリームプロセスは入力をバッファリングできます。これは、入力の最後の行がダウンストリームパイプラインにかかる可能性があることを意味します。以下のバージョンは非ブロック読み取りを使用し、入力ブロックに改行を挿入します。

STDOUT.sync                                   # no outbound buffering by Ruby
buf=''
def read_from_stdin()
  last=''
  while true
    begin
      c = STDIN.read_nonblock(1)              # read 1 character; don't block
    rescue Errno::EWOULDBLOCK                 # exception if nothing to read
      yield "\n" unless last=="\n"            # send a newline if prior character wasn't
      IO.select([STDIN])                      # block (wait for input)
      retry                                   # go back to 'begin' again
    end 
    yield last=c                              # remember and send read character
  end 
end

read_from_stdin do |c| 
  if buf.length>0 || c=='<'                    # buffering starts when '<' received
    buf << c                                   #        and continues until flushed
    buf.gsub!(/(<\d+>)/,"\n\\1") if (c == '>') # regex transform matching buffer
    unless (buf =~ /<\d*$/)                    # flush buffer when regex fails
      STDOUT << buf 
      buf.replace ''                           # empty buffer stops buffering
    end 
  else
    STDOUT << c;                               # unbuffered output
  end 
  STDOUT.flush                                 # no buffering, please!
end

関連情報