🔁

文字とコードポイントを相互変換するコマンドを作成する

に公開

文字とコードポイントを相互変換する処理はシェル芸のよくある題材です。テストケースでこれらの相互変換をよく使うので Bash、Python、Rust でコマンドとして作成してみました。

Bash での実装

ordchr ファイルを用意して次のコードを記載します。

ord
input="$1"
echo -n "$input" \
  | grep -oP . \
  | while IFS= read -r char; do
      printf '%X ' "'$char"
    done
echo
chr
for cp in "$@"; do
  printf '%b' "\\U$(printf '%08X' 0x$cp)"
done
printf '\n'

ord では grep が使われていますが、UTF-8 ロケールであればインデックスアクセスが利用できます。

$ export LC_CTYPE=en_US.UTF-8

$ str="あいうえお"
$ echo "${#str}"
5     # ← 文字数としてカウント!

$ echo "${str:0:1}"
あ    # ← 正しく1文字を取り出している

実行権限を付与します。

chmod +x ord
chmod +x chr

ord と chr を PATH が通っている場所にインストールします。

基本的な機能が使えるか確認します。

> ord あ
3042 
> chr 3042
あ

複数の文字やコードポイントにも対応しています。

> ord あいうえお
3042 3044 3046 3048 304A 
> chr 3042 3044 3046 3048 304A
あいうえお

Python による実装

移植性を考慮して Python による実装を示します。

ord
#!/usr/bin/env python3

import sys

text = sys.argv[1] if len(sys.argv) > 1 else ""
for char in text:
    print(f"{ord(char):X}", end=" ")
print()
chr
#!/usr/bin/env python3
import sys

def cps_to_chars(codepoints):
    chars = []
    for cp in codepoints:
        cp = cp.upper().lstrip("U+")
        try:
            chars.append(chr(int(cp, 16)))
        except ValueError:
            print(f"無効なコードポイント: {cp}", file=sys.stderr)
            sys.exit(1)
    return ''.join(chars)

if __name__ == "__main__":
    if sys.stdin.isatty():
        cps = sys.argv[1:]
    else:
        cps = sys.stdin.read().strip().split()

    print(cps_to_chars(cps))

Rust による実装

1つのプロジェクトで2つのコマンドツールを作成してみましょう。charutil というフォルダーを作成します。

Cargo.toml
[package]
name = "charutil"
version = "0.1.0"
edition = "2021"

[dependencies]
src/bin/ord.rs
fn main() {
    let input = std::env::args().nth(1).unwrap_or_default();
    for c in input.chars() {
        print!("{} ", char_to_hex(c));
    }
    println!();
}

fn char_to_hex(c: char) -> String {
    format!("{:X}", c as u32)
}
src/bin/chr.rs
use std::env;
use std::io::{self, Read};

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();
    let input = if !args.is_empty() {
        args.join(" ")
    } else {
        let mut buf = String::new();
        io::stdin().read_to_string(&mut buf).unwrap();
        buf
    };

    for hex in input.split_whitespace() {
        match u32::from_str_radix(hex.trim_start_matches("U+"), 16)
            .ok()
            .and_then(std::char::from_u32)
        {
            Some(c) => print!("{}", c),
            None => eprintln!("Invalid codepoint: {}", hex),
        }
    }
    println!();
}

ビルドします。

cargo build

個別にビルドすることもできます。

cargo build --bin ord
cargo build --bin chr

テストします。

> target/debug/ord あ漢🍣
3042 6F22 1F363 
> target/debug/chr 3042 6F22 1F363
あ漢🍣
> echo "3042 6F22 1F363" | target/debug/chr
あ漢🍣
GitHubで編集を提案

Discussion