今回はFXとは直接関係のない技術的なメモです。
概要
- 日本語などのマルチバイト文字を含む文字列をシェルで1文字ずつ改行する/1文字ずつ取り出して処理するには、
fold
よりもgrep -o .
やwhile read -N1
を用いるほうがよい。 - その動作はロケールや環境に依存する。
ASCII文字のみの場合
シェルスクリプト(BASH)で、与えられた文字列から1文字ずつ改行したり、1文字ずつ取り出して処理したいことがあります。方法としては、例えばfold
を使って文字列を1文字ずつ折り返す、grep
で1文字ずつマッチさせる、read
で1文字ずつ読み取りループする、awk, perl
などのスクリプト言語を用いるなどの方法が考えられます。
処理対象の文字列が印字可能なASCII文字のみの場合は、どの方法でもうまくいきます。1文字ずつ改行するのではなく、1文字ごとに何か処理を行いたい場合には、最後の例で、echo "$c"
としている部分を、目的の処理に置き換えればOKです。
echo abc | fold -w 1
# a
# b
# c
echo abc | \grep -o .
# a
# b
# c
echo -n abc | while read -N1 c; do echo "$c"; done
# a
# b
# c
マルチバイト文字の場合
日本語などのマルチバイト文字が含まれる場合には、環境により動作が変わります。以下、Rocky Linux 9.2での実行例です。UTF-8 ロケールを用いている場合問題なく実行できますが、UTF-8ロケールでない場合は当然ながら正しく動作しません。
export LANG=C.UTF-8
locale
# LANG=C.UTF-8
# LC_CTYPE="C.UTF-8"
# (略)
# LC_ALL=
s="abc漢字😄"
echo $s | fold -w 1
# a
# b
# c
# 漢
# 字
# 😄
echo $s | \grep -o .
# foldと同じ正しい結果なので略
echo -n $s | while read -N1 c; do echo "$c"; done
# foldと同じ正しい結果なので略
export LANG=C
locale
# LANG=C
# LC_CTYPE="C"
# (略)
# LC_ALL=
echo $s | fold -w 1
# a
# b
# c
# 以下文字化け
echo $s | \grep -o .
# foldと同じ文字化けした結果なので略
echo -n $s | while read -N1 c; do echo "$c"; done
# foldと同じ文字化けした結果なので略
FreeBSD 13.2での実行結果も、Rocky Linux 9.2 での結果とほぼ同じでした。UTF-8ロケールでは期待通りの動作になります。非UTF-8ロケールでのBSD版fold
の結果のみ、Rocky Linuxとは若干異なる結果になりますが、これはマルチバイト文字列の表示幅が0だとして扱われるため、マルチバイト文字列の途中で改行されないためです。
locale
# LANG=C
# LC_CTYPE="C"
# (略)
# LC_ALL=
echo $s | fold -w 1
# a
# b
# c漢字😄
Ubuntu 22.03 LTSで実行すると、UTF-8ロケールでのfold
の結果が異なることがわかります。
export LANG=C.UTF-8
locale
# LANG=C.UTF-8
# LC_CTYPE="C.UTF-8"
# (略)
# LC_ALL=
s="abc漢字😄"
echo $s | fold -w 1
# a
# b
# c
# 以下文字化け
echo $s | \grep -o .
# a
# b
# c
# 漢
# 字
# 😄
echo -n $s | while read -N1 c; do echo "$c"; done
# grepと同じ正しい結果なので略
export LANG=C
locale
# LANG=C
# LC_CTYPE="C"
# (略)
# LC_ALL=
echo $s | fold -w 1
# a
# b
# c
# 以下文字化け
echo $s | \grep -o .
# foldと同じ文字化けした結果なので略
echo -n $s | while read -N1 c; do echo "$c"; done
# foldと同じ文字化けした結果なので略
これは、Red Hat系のOS (Fedoraや互換OSを含む)のfold
には国際化(i18n)対応パッチが適用されているのに対して、Debian/Ubuntu系のOSのfold
には適用されていないためです。国際化対応パッチが適用されているかどうかは、fold が -c オプションをサポートしているかどうかで判別できます。
# Rocky Linux 9.2
fold --help
# Usage: fold [OPTION]... [FILE]...
# Wrap input lines in each FILE, writing to standard output.
#
# With no FILE, or when FILE is -, read standard input.
#
# Mandatory arguments to long options are mandatory for short options too.
# -b, --bytes count bytes rather than columns
# -c, --characters count characters rather than columns
# -s, --spaces break at spaces
# -w, --width=WIDTH use WIDTH columns instead of 80
# --help display this help and exit
# --version output version information and exit
#
# GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
# Report any translation bugs to <https://translationproject.org/team/>
# Full documentation <https://www.gnu.org/software/coreutils/fold>
# or available locally via: info '(coreutils) fold invocation'
# Ubuntu 20.04LTS
fold --help
# Usage: fold [OPTION]... [FILE]...
# Wrap input lines in each FILE, writing to standard output.
#
# With no FILE, or when FILE is -, read standard input.
#
# Mandatory arguments to long options are mandatory for short options too.
# -b, --bytes count bytes rather than columns
# -s, --spaces break at spaces
# -w, --width=WIDTH use WIDTH columns instead of 80
# --help display this help and exit
# --version output version information and exit
#
# GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
# Report any translation bugs to <https://translationproject.org/team/>
# Full documentation <https://www.gnu.org/software/coreutils/fold>
# or available locally via: info '(coreutils) fold invocation'
上記の実験から、日本語などのマルチバイト文字を含む文字列をシェルで1文字ずつ改行する/1文字ずつ取り出して処理するには、fold
を使うのではなく grep -o .
や while read -N1
を用いるほうがよいことがわかります。また、正しい結果を得るには適切なロケール (C.UTF-8
, ja_JP.UTF-8
, en_US.UTF-8
など) を用いる必要があります。
しかし、これはあくまでも今回テストした環境のみについて言えることです。実際にはgrep -o .
や while read -N1
がマルチバイト文字をサポートしない実装も存在し得ます。また、適切なUTF-8ロケールが使えない可能性もあります。例えば、CentOS 7 のように古い glibc を用いている環境ではロケール C.UTF-8 をサポートしていません。
各種コマンドでUTF-8を正しく取り扱うことのできな場合、UTF-8をサポートする何らかのスクリプト言語を使い、入出力がUTF-8であると決め打ちするとよさそうです。例えばPerlの場合次のようなワンライナーで実現できます。-CSD
オプションは入出力がUTF-8であるとみなすオプションです。
locale
# LANG=C
# LC_CTYPE="C"
# (略)
# LC_ALL=
echo $s | perl -CSD -lnE 'for $c (split //) {say $c}'
# a
# b
# c
# 漢
# 字
# 😄
今後は、このようなFXとは直接関係のない、日常的な作業で遭遇するちょっとしたことに関するメモやTIPSについても書いていく予定です。
コメント