シェルスクリプト(少なくとも端末(文字が現在のロケールで正しい幅で表示される端末))で文字列の表示幅を取得する最も近いポータブル方法は何ですか?
主に非制御文字の幅に興味がありますが、バックスペース、キャリッジリターン、水平タブなどの制御文字の解決策も歓迎します。
つまり、私は次を探しています。シェルPOSIX関数を囲むAPI wcswidth()
。
このコマンドは以下を返す必要があります。
$ that-command 'unix' # 4 fullwidth characters
8
$ that-command 'Stéphane' # 9 characters, one of which zero-width
8
$ that-command 'もで 諤奯ゞ' # 5 double-width Japanese characters and a space
11
列に埋め込まれた文字幅を考慮するを使用するか、ksh93
たとえば、少なくともText::CharWidthモジュールがありますが、より直接的または移植可能な方法はありますか? ?printf '%<n>Ls'
<n>
col
printf '++%s\b\b--\n' <character> | col -b
perl
これはほぼフォローアップです。別の問題これは画面の右側にテキストを表示することに関するものであるため、テキストを表示する前にその情報を取得する必要があります。
答え1
1行文字列の場合、GNU実装には必要な操作を正確に実行する(制御文字を除く)オプションがありますwc
。-L
--max-line-length
答え2
ターミナルエミュレータでは、カーソル位置レポートを使用して前後の位置を取得できます。
...record position
printf '%s' $string
...record position
端末に印刷された文字の幅がどのくらいになるかを確認してください。これはECMA-48(およびVT100)制御シーケンスなので、使用しているほぼすべての端末がこれをサポートしているため、移植性に優れています。
参考までに
CSI Ps nデバイスステータスレポート(DSR)。 ... Ps = 6 -> カーソル位置 (CPR) [行;列] レポート。 結果はCSI r 受容体である。
最終的に、端末エミュレータは、次の要因によって印刷可能な幅を決定します。
- ロケールは文字列形式に影響しますが、端末に送信される一連のバイトは端末の構成方法に基づいて解釈されます(一部の人はUTF-8でなければならないと思うかもしれませんが、移植性質問で求めた機能です)。
wcswidth
POSIXだけでは、関数の説明ではこれらの側面に言及しません。- 当然だと考えられるいくつかの単一の幅文字(線の描画など)は(ユニコードで)「あいまいな幅」であり、スタンドアロン
wcswidth
アプリケーションの移植性を破壊します(例:第2章 Cygwinの設定)。xterm
たとえば、必要な設定に2バイト文字を選択できます。 - 印刷可能な文字以外のものを処理するには、ターミナルエミュレータを使用する必要があります(エミュレートする場合を除く)。
wcswidth
Shell API呼び出しはさまざまなレベルでサポートされています。
- Text::CharWidth - 端末で文字列が占める列数を取得します。
このモジュールは機能を提供します似たようなたとえば、C言語のwcwidth(3)とwcswidth(3)です。
- 議論するルビヨン
- Python API
これはやや簡単です。wcswidth
Perlの場合は、エミュレーション、Ruby、PythonでCランタイムを呼び出します。 Pythonのように(文字の組み合わせを処理できる)呪いを使用することもできます。
- 初期化端末の使用設定項目(画面にテキストは記録されません)
- 使用
filter
機能(単一行用) - 行の先頭にテキストを描く
addstr
、エラーがないか確認し(長い場合)、終了位置を確認してください。 - スペースがある場合は、開始位置を調整します。
- 呼ぶ
endwin
(こうしてはいけない。refresh
) - 開始位置に関する結果情報を標準出力に書き込みます。
呪いを使う出力(情報をスクリプトに送り返すか直接呼び出すのではなくtput
)、行全体を消去します(filter
実際には1行に制限)。
答え3
私は.profile
端末の文字列の幅を決定するスクリプトを呼び出します。システムセットを信頼していないマシンコンソールにログインしたときLC_CTYPE
、またはリモートでログインしていてリモート側との一致を信頼できない場合にLC_CTYPE
このオプションを使用します。私のスクリプトはライブラリを呼び出すのではなく、端末に問い合わせます。これは私のユースケースの中心だからです。端末のエンコーディングを決定することです。
これは多くの点で脆弱です。
- ディスプレイを変更するので、ユーザーエクスペリエンスはあまり良くありません。
- 他のプログラムが間違った時間に何かを表示すると、競合状態が発生します。
- 端末が応答しない場合はロックされます。 (数年前これを改善する方法をお問い合わせください。しかし、実際にはそれほど大きな問題ではなかったので、そのソリューションに切り替えるつもりは全くありませんでした。端末が応答しない唯一の状況は、Windows Emacsを使用して
plink
Linuxシステムからリモートファイルにアクセスすることでした。plinkx
代わりにこの方法を使用してください.)
これはあなたのユースケースに適しているかもしれませんし、そうでないかもしれません。
#! /bin/sh
if [ z"$ZSH_VERSION" = z ]; then :; else
emulate sh 2>/dev/null
fi
set -e
help_and_exit () {
cat <<EOF
Usage: $0 {-NUMBER|TEXT}
Find out the width of TEXT on the terminal.
LIMITATION: this program has been designed to work in an xterm. Only
xterm and sufficiently compatible terminals will work. If you think
this program may be blocked waiting for input from the the terminal,
try entering the characters "0n0n" (digit 0, lowercase letter n,
repeat).
Display TEXT and erase it. Find out the position of the cursor before
and after displaying TEXT so as to compute the width of TEXT. The width
is returned as the exit code of the program. A value of 100 is returned if
the text is wider than 100 columns.
TEXT may contain backslash-escapes: \\0DDD represents the byte whose numeric
value is DDD in octal. Use '\\\\' to include a single backslash character.
You may use -NUMBER instead of TEXT (if TEXT begins with a dash, use
"-- TEXT"). This selects one of the built-in texts that are designed
to discriminate between common encodings. The following table lists
supported values of NUMBER (leftmost column) and the widths of the
sample text in several encodings.
1 ASCII=0 UTF-8=2 latinN=3 8bits=4
EOF
exit
}
builtin_text () {
case $1 in
-*[!0-9]*)
echo 1>&2 "$0: bad number: $1"
exit 119;;
-1) # UTF8: {\'E\'e}; latin1: {\~A\~A\copyright}; ASCII: {}
text='\0303\0211\0303\0251';;
*)
echo 1>&2 "$0: there is no text number $1. Stop."
exit 118;;
esac
}
text=
if [ $# -eq 0 ]; then
help_and_exit 1>&2
fi
case "$1" in
--) shift;;
-h|--help) help_and_exit;;
-[0-9]) builtin_text "$1";;
-*)
echo 1>&2 "$0: unknown option: $1"
exit 119
esac
if [ z"$text" = z ]; then
text="$1"
fi
printf "" # test that it is there (abort on very old systems)
csi='\033['
dsr_cpr="${csi}6n" # Device Status Report --- Report Cursor Position
dsr_ok="${csi}5n" # Device Status Report --- Status Report
stty_save=`stty -g`
if [ z"$stty_save" = z ]; then
echo 1>&2 "$0: \`stty -g' failed ($?)."
exit 3
fi
initial_x=
final_x=
delta_x=
cleanup () {
set +e
# Restore terminal settings
stty "$stty_save"
# Restore cursor position (unless something unexpected happened)
if [ z"$2" = z ]; then
if [ z"$initial_report" = z ]; then :; else
x=`expr "${initial_report}" : "\\(.*\\)0"`
printf "%b" "${csi}${x}H"
fi
fi
if [ z"$1" = z ]; then
# cleanup was called explicitly, so don't exit.
# We use `trap : 0' rather than `trap - 0' because the latter doesn't
# work in older Bourne shells.
trap : 0
return
fi
exit $1
}
trap 'cleanup 120 no' 0
trap 'cleanup 129' 1
trap 'cleanup 130' 2
trap 'cleanup 131' 3
trap 'cleanup 143' 15
stty eol 0 eof n -echo
printf "%b" "$dsr_cpr$dsr_ok"
initial_report=`tr -dc \;0123456789`
# Get the initial cursor position. Time out if the terminal does not reply
# within 1 second. The trick of calling tr and sleep in a pipeline to put
# them in a process group, and using "kill 0" to kill the whole process
# group, was suggested by Stephane Gimenez at
# https://unix.stackexchange.com/questions/10698/timing-out-in-a-shell-script
#trap : 14
#set +e
#initial_report=`sh -c 'ps -t $(tty) -o pid,ppid,pgid,command >/tmp/p;
# { tr -dc \;0123456789 >&3; kill -14 0; } |
# { sleep 1; kill -14 0; }' 3>&1`
#set -e
#initial_report=`{ sleep 1; kill 0; } |
# { tr -dc \;0123456789 </dev/tty; kill 0; }`
if [ z"$initial_report" = z"" ]; then
# We couldn't read the initial cursor position, so abort.
cleanup 120
fi
# Write some text and get the final cursor position.
printf "%b%b" "$text" "$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
initial_x=`expr "$initial_report" : "[0-9][0-9]*;\\([0-9][0-9]*\\)0" || test $? -eq 1`
final_x=`expr "$final_report" : "[0-9][0-9]*;\\([0-9][0-9]*\\)0" || test $? -eq 1`
delta_x=`expr "$final_x" - "$initial_x" || test $? -eq 1`
cleanup
# Zsh has function-local EXIT traps, even in sh emulation mode. This
# is a long-standing bug.
trap : 0
if [ $delta_x -gt 100 ]; then
delta_x=100
fi
exit $delta_x
スクリプトは、戻り状態で100に切り捨てられた幅を返します。使用例:
widthof -1
case $? in
0) export LC_CTYPE=C;; # 7-bit charset
2) locale_search .utf8 .UTF-8;; # utf8
3) locale_search .iso88591 .ISO8859-1 .latin1 '';; # 8-bit with nonprintable 128-159, we assume latin1
4) locale_search .iso88591 .ISO8859-1 .latin1 '';; # some full 8-bit charset, we assume latin1
*) export LC_CTYPE=C;; # weird charset
esac
答え4
私の問題でとを使用してcol
可能な解決策を拡張するためのヒント:ksh93
コントロールではなく単一文字の幅を取得するには、Debian からcol
from を使用しますbsdmainutils
(他の実装では動作しない可能性があります):col
charwidth() {
set "$(printf '...%s\b\b...\n' "$1" | col -b)"
echo "$((${#1} - 4))"
}
例:
$ charwidth x
1
$ charwidth $'\u301'
0
$ charwidth $'\u94f6'
2
文字列に拡張:
stringwidth() {
awk '
BEGIN{
s = ARGV[1]
l = length(s)
for (i=0; i<l; i++) {
s1 = s1 ".."
s2 = s2 "\b\b"
}
print s1 s s2 s1
exit
}' "$1" | col -b | awk '
{print length - 2 * length(ARGV[2]); exit}' - "$1"
}
使用ksh93
:printf '%Ls'
charwidth() {
set "$(printf '.%2Ls.' "$1")"
echo "$((5 - ${#1}))"
}
stringwidth() {
set "$(printf '.%*Ls.' "$((2*${#1}))" "$1")" "$1"
echo "$((2 + 3 * ${#2} - ${#1}))"
}
使用perl
:Text::CharWidth
stringwidth() {
perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' -- "$@"
}