String(format:)
日本語環境の macOS では Foundation.framework の NSString(format:) およびこれを内部的に呼ぶ String(format:) で %s フォーマット指定子を利用して C 文字列を変換すると、バックスラッシュ (= \ = 0x5C) が円マーク (= ¥ = 0xA5) になります。
print("\\".withCString { String(format: "%s", $0) })
// => ¥
フォーマット指定子を利用せずテンプレートにリテラルの \ を書いた場合は変換されません。
print("\\".withCString { String(format: "\\%s", $0) })
// => \¥
ちなみに \ の ASCII コードポイントを直接指定しても同じく ¥ に変換されます。
print("\u{5C}".withCString { String(format: "%s", $0) })
// => ¥
いずれも英語環境では問題なく \ になります。
この挙動は NSString(format:) のリファレンス や String Programming Guide for Core Foundation などには記載がありません。
Because the %s specifier causes the characters to be interpreted in the system default encoding, the results can be variable, especially with right-to-left languages.
という記載があり、%s 指定子はシステムの default encoding の影響を受けると明記されています。
システムの default encoding は以下のコードで確認できます。
私の環境では以下のようになりました。
print(String.defaultCStringEncoding) // => Japanese (MacOS)
Japanese (MacOS) とは CFStringEncodings.macJapanese を指しています。
assert(String.defaultCStringEncoding.rawValue == CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.macJapanese.rawValue)))
MacJapanese は Shift_JIS の拡張なので、0x5C は ASCII と互換性がなく、¥ となっています。
これをプログラム側から変更することはできません。
https://developer.apple.com/documentation/foundation/nsstring/1410091-defaultcstringencoding
The default C-string encoding is determined from system information and can’t be changed programmatically for an individual process.
これを変更するには、プログラム起動時に環境変数 __CFSTRING_TEXT_ENCODING の値を変更してあげる必要があります。
起動後にプログラム自身が環境変数を変更しても更新はされません。
$ cat main.swift
print("\\".withCString { String(format: "%s", $0) })
$ __CFSTRING_TEXT_ENCODING=0x1F5:0x08000100:0xE swift
\
試していませんが、おそらく MacJapanese だけでなく、0x5C が ASCII と互換性のない文字コード (e.g. KS X 1001) では同じ問題が起こりそうです。
プログラム側からユーザーの環境を制御することはできないので、ユーザーからの入力を String(format: "%s") に渡さないようにしなくてはなりません。
String(utf8String:)
これは NSString(format:) 特有の事象であるようで、NSString(cString:encoding:) や NSString(utf8String:) では発生しないようです。
print("\\".withCString { String(utf8CString: $0)! })
// => \
print("\\".withCString { String(cString: $0, encoding: .utf8)! })
// => \
printf()
また、似た機能の C の関数群 printf ファミリーでは同じ現象は発生しません。
#include <stdio.h>
int main(int argc, char **argv) {
printf("%s", "\\");
// => \
return 0;
}
String(format:locale:)
String(format:) にはロケールを設定できるので、システムロケールの問題かと思い POSIX ロケールを明示的にセットしてみましたが、効果はありませんでした。
一応 setlocale() もしてみましたが、効果はなさそうです。
let locale = Locale(identifier: "C")
print("\\".withCString { String(format: "%s", locale: locale, $0) })
// => ¥
setlocale(LC_ALL, "C");
print("\\".withCString { String(format: "%s", $0) })
// => ¥
まとめ
文字列のフォーマットに String(format:) を使用した場合は、うまい回避方法が見つけられていません。
扱う文字列が ASCII だけで良い場合は snprintf など C 言語の層で変換を行ってから String に変換するのが良いでしょう。
もし ASCII 外の文字列を扱う場合は String.replacingOccurrences(of:with:) などを用いて、 ¥ を \ に変換してあげる必要があるかもしれません。