シェルで文字列を1文字ずつ改行する/1文字ずつ取り出して処理する

Linux

今回は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についても書いていく予定です。

本サイトの内容は、投資の勧誘を目的としたものではなく、本サイト内の情報に基づいて行った取引の損失について、本サイトは一切の責を負いかねます。当該情報の欠落・誤謬等につきましてもその責を負いかねますのでご了承ください。免責事項もご覧ください。

Linux

コメント

タイトルとURLをコピーしました