ls で日本語の幅を正しく計算させてみる

Darwinソースコードを入手・改変して、ls コマンドで1文字につき半角文字3つ分として計算される全角文字を半角2つぶんで表示させることに挑戦してみました。
wchar.h の関数を使うのは今回が初めてだったりします。
そのまま make しようとすると membership.h が無い旨のエラーが出るので Libinfo-*/membership.subproj からヘッダファイルをコピーして make します。

diff -u darwin8-file_cmds-ls/ls.c ls-wc/ls.c
--- darwin8-file_cmds-ls/ls.c	2004-11-16 11:47:38.000000000 +0900
+++ ls-wc/ls.c	2007-04-06 22:44:26.000000000 +0900
@@ -68,6 +68,7 @@
 #include <termcap.h>
 #include <signal.h>
 #endif
+#include <wchar.h>
 #ifdef __APPLE__
 #include <get_compat.h>
 #else
@@ -630,6 +631,11 @@
 	bcfile = 0;
 	flags = NULL;
 	for (cur = list, entries = 0; cur; cur = cur->fts_link) {
+		wchar_t wc;
+		int mbclen;
+		int offset;
+		u_long namewidth;
+
 		if (cur->fts_info == FTS_ERR || cur->fts_info == FTS_NS) {
 			warnx("%s: %s",
 			    cur->fts_name, strerror(cur->fts_errno));
@@ -654,8 +660,22 @@
 				continue;
 			}
 		}
-		if (cur->fts_namelen > maxlen)
-			maxlen = cur->fts_namelen;
+		/* Get wcwidth */
+		namewidth = 0;
+		offset = 0;
+		mbclen = mbtowc(&wc, cur->fts_name, MB_CUR_MAX);
+		while (mbclen > 0) {
+			namewidth += wcwidth(wc);
+			offset += mbclen;
+			mbclen = mbtowc(&wc, cur->fts_name + offset, MB_CUR_MAX);
+		}
+		if (mbclen != -1) {
+			if (namewidth > maxlen)
+				maxlen = namewidth;
+		} else {
+			if (cur->fts_namelen > maxlen)
+				maxlen = cur->fts_namelen;
+		}
 		if (f_octal || f_octal_escape) {
 			u_long t = len_octal(cur->fts_name, cur->fts_namelen);
 
Only in ls-wc: memberd_defines.h
Only in ls-wc: membership.h
Only in ls-wc: membershipPriv.h
Only in ls-wc: ntsid.h
diff -u darwin8-file_cmds-ls/print.c ls-wc/print.c
--- darwin8-file_cmds-ls/print.c	2005-03-11 07:14:12.000000000 +0900
+++ ls-wc/print.c	2007-04-06 22:57:18.000000000 +0900
@@ -69,6 +69,7 @@
 #include <termcap.h>
 #include <signal.h>
 #endif
+#include <wchar.h>
 
 #include "ls.h"
 #include "extern.h"
@@ -157,8 +158,24 @@
 		return prn_octal(name);
 	else if (f_nonprint)
 		return prn_printable(name);
-	else
-		return printf("%s", name);
+	else {
+		wchar_t wc;
+		int mbclen, offset, width;
+
+		width = 0;
+		offset = 0;
+		mbclen = mbtowc(&wc, name, MB_CUR_MAX);
+		while (mbclen > 0) {
+			width += wcwidth(wc);
+			offset += mbclen;
+			mbclen = mbtowc(&wc, name + offset, MB_CUR_MAX);
+		}
+		if (width > 0 && mbclen != -1) {
+			(void)printf("%s", name);
+			return width;
+		} else
+			return printf("%s", name);
+	}
 }
 
 /*

このパッチを当てた ls では (ロケールが ja_JP.UTF-8 なら) ls -v で正しい幅で表示される・・・と思いきや、濁点があるファイル名でズレが発生。Mac OS Xファイルシステム文字コードは NFKD で正規化された UTF-8 なわけですが、wcwidth() が返す値は合成用の文字でも 0 ではない模様。そもそも上記のコードでは 0 を返されては困るわけですが。
今回は標準 C ライブラリの関数を使いましたが、Markus Kuhn 氏の wcwidth.c もしくは ICU を使ったほうが良さげです。


追記:
やっつけパッチため、ロケールUTF-8 でないとちゃんと表示できないです。