Shell文字列処理メモ(Linux)

## 概要

Linuxで行う文字列処理のメモです。
Ubuntuでなるべくプリインストールされたコマンドを使っています。

## 行フィルター

### パスワード生成

```console
$ pwgen --symbols 12 1
Tu;qu^aey1ue
```

### パスワードを差しさわりのない文字に置き換える

パスワードを差しさわりのない文字に置き換えるか、文字数分のダミー文字列を生成する。

```console
$ echo "Tu;qu^aey1ue" | sed -e 's/./*/g'
************
$ python3 -c "print('*' * 12)"
************
$ ruby -e "puts('*' * 12)"
************
```


### uuidを差しさわりのない文字に置き換える

```
$ uuidgen 
c2663006-4033-4f6f-bd20-4044eca0ac28
$ uuidgen | tr '[0-9a-f]'
000000000-0000-0000-0000-000000000000
```

## 行単位の処理

### 特定のパターンに一致した行を置換する

```shell
printf "Cloudy in Tokyo.\nCloudy in Kyoto.\nCloudy in Osaka.\n" | sed -e '/Tokyo/s/Cloudy/Sunny/'
```

```
Sunny in Tokyo.
Cloudy in Kyoto.
Cloudy in Osaka.
```

### 特定のパターンに一致した行以外を置換する

```shell
printf "Cloudy in Tokyo.\nCloudy in Kyoto.\nCloudy in Osaka.\n" | sed -e '/Tokyo/!s/Cloudy/Sunny/'
```

```
Cloudy in Tokyo.
Sunny in Kyoto.
Sunny in Osaka.
```

### 特定の行を除外する

`Banana` と `Durian` を除外する。

* 入力例
    
    ```
    Apple
    Banana
    Cherry
    Durian
    ```

* 出力結果

    ```
    Apple
    Cherry
    ```

* sed: 行を削除する  
    [`Apple`, `Banana`, `Cherry`, `Durian`] → [`Apple`, `Cherry`] のようになる

    ```console
    $ printf "Apple\nBanana\nCherry\nDurian\n" | sed -e '/\(Banana\|Durian\)/d'
    Apple
    Cherry
    $ printf "Apple\nBanana\nCherry\nDurian\n" | sed -ne '/\(Banana\|Durian\)/d; p'
    Apple
    Cherry
    ```

    削除したリストを続けて処理できる。

    ```console
    $ printf "Apple\nBanana\nCherry\nDurian\n" | sed -e '/\(Banana\|Durian\)/d; s/Apple/Apple2/'
    Apple2
    Cherry
    ```

* sed: マッチしなかった行を「表示」する ※(2025-05-27)コメントを頂いたので訂正  
  
  `d`の場合と異なり、表示処理された行は削除されない。
    [`Apple`, `Banana`, `Cherry`, `Durian`] → [`Apple`, `Banana`, `Cherry`, `Durian`]

    ```shell
    printf "Apple\nBanana\nCherry\nDurian\n" | sed -ne '/\(Banana\|Durian\)/!p'
    ```
    
    次のコマンドにより、表示されなかった Banana の行が残っていることが分かります。

    ```console
    $ printf "Apple\nBanana\nCherry\nDurian\n" | sed -ne '/\(Banana\|Durian\)/!p; /Banana/p'
    Apple
    Banana
    Cherry
    ```

    例えば、ログからERROR行を表示して、INFO行を除外して `w` でファイルに出力といったことができます。

    ```
    $ printf "INFO: aaa\nWARNING: bbb\nERROR: ccc\nINFO: ddd\n" | sed -ne '/ERROR/p; /INFO/d; w out.txt'
    ERROR: ccc
    $ cat out.txt
    WARNING: bbb
    ERROR: ccc
    ```

* Ruby

    ```shell
    printf "Apple\nBanana\nCherry\nDurian\n" | ruby -e "STDIN.each_line(chomp: true){|v| puts v if ! ['Banana', 'Durian'].include?(v)}"
    ```

* Python

    ```shell
    printf "Apple\nBanana\nCherry\nDurian\n" | python3 -c "import sys; [print(line) for line in list(filter(lambda x: x not in ['Banana', 'Durian'], [line.rstrip('\n') for line in sys.stdin.readlines()]))]"
    ```

## テキストで行をまたがる処理

### 行末に空白文字があるファイルを探す

* 行末に空白文字があるファイルを探す

    ```shell
    grep -nr '[[:blank:]]$' --include='*.rs' .
    ```

    ```shell
    find . -type f -name '*.rs' -exec egrep -l " +$" {} \;
    ```

### テキストの先頭に行を追加する

* catを使う  
  `-` は標準入力から読み取ります。そのあと、ファイルから読み取ります。

    ```shell
    #!/bin/bash
    TMP_FILE=$(mktemp $0-XXX.txt) && {
        cat > $TMP_FILE <<EOF
    Cherry
    Durian
    EOF
        printf "Apple\nBanana\n" | cat - $TMP_FILE
        rm -f $TMP_FILE
    }
    ```

* sedを使う

    ```shell
    #!/bin/bash
    TMP_FILE=$(mktemp $0-XXX.txt) && {
        cat > $TMP_FILE <<EOF
    Cherry
    Durian
    EOF
        sed -e '1s/^/Apple\nBanana\n/' $TMP_FILE
        rm -f $TMP_FILE
    }
    ```

### マッチした次の行を置換する

パターンの後に `{}` でブロックをかける。ブロックの中で `n` が次の行を示す。

* コード例

    ```shell
    printf "remote:\n  - id: primary\nkey:\n  - id: foo\nacl:\n  - id: update_acl\n" | sed -e '/^key:/{n; s/id: .*/id: UUUUUU/}'
    ```

* 入力例

    ```yaml
    remote:
      - id: primary
    key:
      - id: foo
    acl:
      - id: update_acl
    ```

* 出力例

    ```yaml
    remote:
      - id: primary
    key:
      - id: UUUUUU
    acl:
      - id: update_acl
    ```

## テーブル処理

### 指定の列を取り出す

4列目を取り出す例。

小数点を含む計算、データを振り分けて集計する、など複雑化してきたら、ほかの言語での実装を検討しましょう。
どの環境でも使用できるメリットを考えれば Python が第一候補で、数値計算にも対応しやすいのは利点。
メソッドチェーンでわりと直観かつ簡潔に記述したいなら Ruby でもよい。

* 入力

    ```console
    $ ps
        PID TTY          TIME CMD
      79597 pts/10   00:00:00 bash
      89456 pts/10   00:00:00 ps
    ```

* Bash配列  
  入力データが単純なスペース区切りの場合は、Bash配列のほうが楽(awkの知識を不要とできる)になる場合がある。

    ```shell
    ps | while read LINE; do
        a=($LINE)
        echo "${a[3]}"
    done
    ```

* GAWK  
  取り出した列そのものが出力結果だったり、区切り文字の指定が必要な場合は GAWK が便利。

    ```shell
    ps | awk '{print $4}'
    ```