29737


シェルスクリプトで、標準入力から、変数sepで指定される ASCII 文字が何文字あるか計算したくなりました。これは、どうやったら実現できますか？


CC BY-SA 3.0
2016-10-19T15:27:28.530
29715
2016-10-23T00:58:35.280
2016-10-19T16:33:24.063
754
754
question
sh unix
標準入力の中に、変数で与えられる ascii 文字が何個あるかを計算したい


`$sep` に改行を含まない前提でよいならこんな感じ? (適当)

[code]

    #!/bin/sh
    sep="$1"
    sed "s/[^`echo "$sep" |sed 's/\\(.\\)/\\\\\\1/g'`]*//g" |tr -d '\n' |wc -c
    
[/code]


CC BY-SA 3.0
2016-10-19T16:31:34.340
29718
2016-10-19T16:31:34.340
3061
29715
answer


awk を使う場合です。

[code]

    $ wget -O J02050.fasta 'http://www.ebi.ac.uk/ena/data/view/J02050&display=fasta&download=fasta&filename=J02050.fasta'
    
    $ sep='ATG'
    $ cat J02050.fasta |
      sep="$sep" awk -vRS='^$' '
        BEGIN{
          sep=ENVIRON["sep"]
          gsub(/[][^\\-]/, "\\\\&", sep)
          getline f
          print gsub("[" sep "]", "", f)
        }
      '
    175
    
[/code]

※ awk の `-v` オプションで `sep` シェル変数を渡す場合、`\n`
などがエスケープシーケンスとして解釈されてしまうが、環境変数経由で渡すとそのまま(`\`(バックスラッシュ) + `n`)となる

[code]

    $ sep='\n'
    $ echo '\nabcn\' | sep="$sep" awk ...
    => 4
    
    $ sep=$'\n' # newline
    $ echo '\nabcn\' | sep="$sep" awk ...
    => 1
    
[/code]

**番外編(GNU grep + GNU sed + GNU coreutils)**

GNU grep の `-z` オプション(ファイル全体を一論理行として処理)、GNU sed の `-z` オプション( `NUL(\0)` を論理行の
separator として処理)を利用。

[code]

    $ grep --version
    grep (GNU grep) 2.25
    $ sed --version
    sed (GNU sed) 4.2.2
    
    $ sep='\n'
    $ echo '\nabcn\' | 
      grep -Pzo "$(printf '[%s]' "$(printf '%s' "$sep" |
                   sed -z 's/[][^\\-]/\\&/g;s/\n/\\n/')")" |
      tr -d '\0' | wc -c
    => 4
    
    $ sep=$'\n' # newline
    $ echo '\nabcn\' ...
    => 1
    
    $ sep=$'\n' # newline
    $ echo -n '\nabcn\' ...
    => 0
    
[/code]

以下は yaegashi さんのヒストグラムスクリプトのマネ。

[code]

    $ sep=$'ATGC\n'
    $ tail -n +2 J02050.fasta |
      grep -Pzo "$(printf '[%s]' "$(printf '%s' "$sep" |
                   sed -z 's/[][^\\-]/\\&/g;s/\n/\\n/')")" |
      sort -z | uniq -zc | sed -z 's/\n/LF/;s/  $/ SP/' | tr '\0' '\n' | cat -t
    
        5 LF
       53 A
       73 C
       73 G
       47 T
    
[/code]


CC BY-SA 3.0
2016-10-19T16:57:49.247
29719
2016-10-22T05:02:23.557
2016-10-22T05:02:23.557
29715
answer


### バイト単位で比較すればよい場合

`sep`が一文字で、改行ではない場合

[code]

    #!/bin/sh
    sep='X'
    fold -bw1 |grep -cFe "$sep"
    
[/code]

`fold` で入力を1byte毎に改行し、`grep`の`-c`で数えます。  
`sep`が正規表現で意味がある文字だと困るので、`-F`も必要。

* * *

`sep`が複数文字であり得る場合

[code]

    #!/bin/sh
    sep='XYZ'
    sep_list="$(printf '%s' "$sep" |fold -bw1)"
    fold -bw1 |grep -cFe "$sep_list"
    
[/code]

`grep`に`-F`を指定した場合、パターンは改行で区切って複数渡せます。（他に複数の`-e`で渡す方法も）  
`sep`を改行区切りのリストにするのにも、`fold`を使用しました。  
`sep`が `-`
で始まっていてもオプションとして扱わせないために、`echo`ではなく`printf`を使い、`grep`には`-e`も必要になります。

* * *

`sep`が改行を含み得る場合

[code]

    #!/bin/sh
    sep='XYZ
    '
    sep_list="$(printf '%s' "$sep" |tr '\n' '\200' |fold -bw1)"
    tr '\n\200' '\200\201' |fold -bw1 |LC_CTYPE=C grep -cFe "$sep_list"
    
[/code]

`grep` は改行を数える事が出来ないので、改行は`tr`で別の文字に変換する事にしました。  
改行は非ASCII文字である`\200`(8進数で指定、適当に選びました）に変換します。  
入力に元からある`\200`は、さらに別の非ASCII`\201`にしてカウント対象外にします。

しかし、UTF-8 の環境で UTF-8 として不正な文字を`grep`に指定するのはまずいようで、かなり遅くなります。  
Tatz Sekine さんのコメントを反映し、ロケールの環境変数を`LC_CTYPE=C` とする事で解決しました。（とりあえず `grep`
にだけ指定しています）

### 文字単位で比較する必要がある場合

例えば Shift-JIS ではマルチバイト文字が ASCII の文字を含んでいます。（「表」が「\」を含んでいるのが有名です）  
こういった場合、UTF-8 などに変換して処理する方法があります。  
あるいは、適切なロケールが設定された環境で（`fold`のようなバイト単位で分けるような物でなく）マルチバイト文字を一文字として認識できる`GNU
sed`のようなツールを使う必要があります。

[code]

    #!/bin/sh
    # GNU sed で動作
    sep='XYZ
    '
    sep_list="$(printf '%s' "$sep" |sed 's/./&\n/g' |sed 's/^$/LF/')"
    sed 's/./&\n/g' |sed 's/^$/LF/' |grep -xcFe "$sep_list"
    
[/code]

上記はついでに、`sep` に ASCII 外の文字も指定可能になるというメリットもありますが、遅いです。

* * *

言語によってはユニコードでも合字を使っており、これは`GNU sed`も一文字と認識してくれないようなので、これを考慮するとユニコードの正規化（例:
`uconv -x any-nfc`）を行なう必要もあります。


CC BY-SA 3.0
2016-10-20T01:06:24.790
29723
2016-10-23T00:58:35.280
2016-10-23T00:58:35.280
3054
3054
29715
answer


`od | sort | uniq -c` でヒストグラムを作って検索するスクリプト:

[code]

    #!/bin/sh
    sep="$1"
    pat="($(set -- $(printf %s "$sep" | od -v -An -tx1) && IFS=\| && echo "$*"))\$"
    od -v -An -tx1 | xargs -n1 | sort | uniq -c | grep -E "$pat"
    
[/code]

実行例:

[code]

    $ echo -e " AB...CD\n abcd " | ./script.sh "aB.C 
    "
          2 0a
          3 20
          3 2e
          1 42
          1 43
          1 61
    
[/code]

`od -w1` を使ってよければ、次のようにして出力をもうすこしわかりやすくできます (微妙ですが)。

[code]

    od -v -An -tax1 -w1 | xargs -n2 | sort -k2 | uniq -c | grep -E "$pat"
    
[/code]

実行例:

[code]

    $ echo -e " AB...CD\n abcd " | ./a.sh "aB.C 
    "
          2 nl 0a
          3 sp 20
          3 . 2e
          1 B 42
          1 C 43
          1 a 61
    
[/code]


CC BY-SA 3.0
2016-10-20T08:26:48.447
29737
2016-10-20T08:46:19.900
2016-10-20T08:46:19.900
9873
9873
29715
answer