文字列の表示幅を取得します


15

シェルスクリプトから文字列の表示幅(少なくとも(現在のロケールの文字を正しい幅で表示する端末上で)表示幅)を取得するポータブルな方法に最も近いもの。

私は主に非制御文字の幅に興味がありますが、バックスペース、キャリッジリターン、水平タブのような制御文字を考慮したソリューションも歓迎します。

言い換えると、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のをprintf '%<n>Ls'考慮にパディングのための文字幅とる<n>列、またはcol(例えばして、コマンドをprintf '++%s\b\b--\n' <character> | col -b試してみて、それを導き出すために)、そこにテキスト:: CHARWIDTHだperl少なくともモジュールが、そこより直接的またはポータブルなアプローチです。

それは多かれ少なかれ、テキストを表示する前にその情報を知る必要がある画面の右側にテキストを表示することに関する他の質問のフォローアップです。


回答:


7

端末エミュレーターでは、カーソル位置レポートを使用して、例えば、

...record position
printf '%s' $string
...record position

端末に表示される文字の幅を確認します。これは、使用する可能性のあるほぼすべての端末でサポートされるECMA-48(およびVT100)制御シーケンスであるため、かなり移植性があります。

参考のために

    CSI Ps nデバイスステータスレポート(DSR)。
              ...
                Ps = 6->レポートカーソル位置(CPR)[行;列]。
              結果はCSI rです。c R

最終的に、ターミナルエミュレータは、次の要因により印刷可能な幅を決定します。

  • ロケール設定は、文字列のフォーマット方法に影響しますが、端末に送信される一連のバイトは、端末の構成方法に基づいて解釈されます(一方で、UTF-8である必要があると主張する人もいます)移植性が質問で要求された機能でした)。
  • wcswidth単独では、結合文字の処理方法はわかりません。POSIXは、その機能の説明でこの側面に言及していません。
  • シングル幅として当然と思われるいくつかの文字(たとえば、線描画)は(Unicodeで) "曖昧な幅"であり、wcswidth単独で使用するアプリケーションの移植性を損ないます(たとえば、第2章Cygwinのセットアップを参照)。 xtermたとえば、これを必要とする構成のために倍幅文字を選択するための規定があります。
  • 印刷可能な文字以外のものを処理するには、ターミナルエミュレータに依存する必要があります(シミュレートしたくない場合)。

シェルAPI呼び出しwcswidthは、さまざまな程度でサポートされています。

これらは多かれ少なかれ直接的です。Perl wcswidthの場合にシミュレートし、RubyとPythonからCランタイムを呼び出します。たとえば、Pythonのcursesを使用することもできます(Pythonは文字の結合を処理します)。

  • setuptermを使用して端末を初期化します(画面にテキストは書き込まれません)
  • filter関数を使用する(単一行用)
  • で行の先頭にテキストを描画し、addstrエラーをチェックして(長すぎる場合)、終了位置を確認します
  • 余裕がある場合は、開始位置を調整します。
  • 呼び出しendwin(を行うべきではありませんrefresh
  • 開始位置に関する結果情報を標準出力に書き込みます

(情報をスクリプトにフィードバックしたり、直接呼び出したりするのではなく)出力に cursesを使用すると、行tput全体がクリアされます(行にfilter制限されます)。


本当にこれが唯一の方法だと思う。端末が倍幅文字をサポートしていない場合は、wcswidth()何について何を言わなくても大丈夫です。
mikeserv

実際にはplink、このメソッドで発生した唯一の問題は、TERM=xtermコントロールシーケンスに応答しない場合でも設定されることです。しかし、私はあまりエキゾチックな端末を使用していません。
ジル 'SO-悪であるのをやめる'

ありがとう。しかし、アイデアは、端末に文字列を表示するにその情報を取得することでした(それを表示する場所を知るために、それは端末の右側に文字列を表示することに関する最近の質問のフォローアップです、おそらく私はそれを言及する必要がありました私の本当の質問は本当にシェルからwcswidthに到達する方法についてでした)。@ mikeserv、yes wcswidth()は、特定の端末が特定の文字列をどのように表示するかについて間違っている可能性がありますが、それは端末に依存しないソリューションに近づくことができ、col / ksh-printfが私のシステムで使用するものです。
ステファンシャゼル

私はそれを認識してんだけど、あまりポータブル機能を経由して除き、直接アクセスすることはできませんwcswidth(あなたはいくつかの仮定を行うことによって、perlでこれを行うことができます-参照search.cpan.org/dist/Text-CharWidth/CharWidth.pmを) 。ところで、右揃えの質問は(おそらく)左下に文字列を書き込んでから、カーソル位置と挿入コントロールを使用して右下にシフトすることで改善できます。
トーマスディッキー

1
@StéphaneChazelas- foldマルチバイトおよび拡張幅の文字を処理するように指定されているようです。バックスペースの処理方法は次のとおりです。現在の行幅のカウントは1ずつ減らされますが、カウントは決して負にはなりません。foldユーティリティは、次の文字の幅が1より大きく、行幅がwidthを超えない限り <backspace>の直前または直後に<newline>を挿入してはなりません。多分何とか一緒に組めるfold -w[num]pr +[num]思いますか?
mikeserv

5

1行の文字列の場合、GNUの実装にwcは、-L--max-line-length制御文字を除く)探しているものを正確に実行する(別名)オプションがあります。


1
ありがとう。ディスプレイの幅が返されるとは思いもしませんでした。FreeBSD実装には-Lオプションもあり、ドキュメントには最長行の文字数が返されると書かれていますが、私のテストでは代わりにバイト数を示しているようです(表示幅ではありません)。OS / Xには-Lはありませんが、FreeBSDから派生すると予想されていました。
ステファンシャゼラス

tab同様に処理するようです(タブが8列ごとに停止することを想定しています)。
ステファンシャゼル

実際、1行以上の文字列の場合、LF制御文字を適切に処理するため、私が探しているものとまったく同じように動作します
ステファンシャゼラス

@StéphaneChazelas:文字数ではなくバイト数を返すという問題がまだありますか?あなたのデータでそれをテストし、あなたが望む結果を得ました:wc -L <<< 'unix'→8、  wc -L <<< 'Stéphane'→8、そして  wc -L <<< 'もで 諤奯ゞ'→11。8文字のように見えますが、そのうちの1つはマルチバイトです。
G-Manは「Reinstate Monica」と言います

@ G-Man、FreeBSD実装について言及していましたが、FreeBSD 12.0およびUTF-8ロケールではまだバイトをカウントしているようです。éは1つのU + 00E9文字またはU + 0065(e)文字とそれに続くU + 0301(鋭アクセントの組み合わせ)を使用して記述できます。後者は質問で示されたものです。
ステファンシャゼラス

4

私には.profile、私は、端末上の文字列の幅を決定するためのスクリプトを呼び出します。system-setを信頼しないマシンのコンソールLC_CTYPEにログインするとき、またはリモートでログインしLC_CTYPE、リモート側と一致することを信頼できないときに、これを使用します。私のスクリプトは、ライブラリを呼び出すのではなく、端末を照会します。これは、私のユースケースの重要なポイントであるためです。端末のエンコードを決定します。

これはいくつかの点で脆弱です。

  • 表示を変更するため、ユーザーエクスペリエンスはあまり良くありません。
  • 別のプログラムが間違った時間に何かを表示する場合、競合状態があります。
  • 端末が応答しない場合はロックします。(数年前、これを改善する方法尋ねましたが、実際にはそれほど問題ではなかったので、そのソリューションに切り替えることはできませんでした。応答しない端末で遭遇した唯一のケースはplinkメソッドを使用してLinuxマシンからリモートファイルにアクセスするWindows Emacsで、代わりにメソッドを使用して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
# /unix/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

これは私にとって役に立ちました(ただし、ほとんどの場合は要約版を使用しました)。printf "\r%*s\r" $((${#text}+8)) " ";末尾に追加することで使用方法を少しきれいにしましたcleanup(8を追加するのは任意です。古いロケールのより広い出力をカバーするのに十分な長さが必要ですが、行の折り返しを避けるには十分に狭くする必要があります)。これはまた何を負うものではありませんが、テストを不可視にする(で結構です行に印刷されている~/.profile
アダム・カッツ

実際には、zshの(5.7.1)であなただけ行うことができます少し実験から表示されtext="Éé"、その後、${#text}(私が得るあなたの表示幅を与える4非Unicode端子INと2ユニコード対応の端末で)。これはbashには当てはまりません。
アダムカッツ

@AdamKatz ${#text}は表示幅を提供しません。現在のロケールで使用されているエンコーディングの文字数を示します。端末のエンコーディングを判断したいので、これは私の目的には役に立ちません。他の理由で表示幅が必要な場合に便利ですが、すべての文字が1ユニット幅ではないため正確ではありません。例えば組み合わせアクセントが0の幅を持っている、と中国の表意文字は、2の幅を持っている
ジル「SO-停止が悪さ」

ええ、良い点。Stéphaneの質問は満たすかもしれませんが、元の意図は満たしていないかもしれません(実際に私もやりたかったので、コードを調整します)。ジルの皆さん、私の最初のコメントがお役に立てば幸いです。
アダムカッツ

3

エリック・プルーイットは、印象的な実装を書いたwcwidth()wcswidth()で利用可能Awkの中をwcwidth.awk。主に4つの機能を提供します

wcscolumns(), wcstruncate(), wcwidth(), wcswidth()

どこwcscolumns()にもない文字を許容します。

$ cat wcscolumns.awk 
{ printf "%d\n", wcscolumns($0) }
$ awk -f wcwidth.awk -f wcscolumns.awk <<< 'unix'
8
$ awk -f wcwidth.awk -f wcscolumns.awk <<< 'Stéphane'
8
$ awk -f wcwidth.awk -f wcscolumns.awk <<< 'もで 諤奯ゞ'
11
$ awk -f wcwidth.awk -f wcscolumns.awk <<< $'My sign is\t鼠鼠'
14

TAB は14より大きいはずなので、TABの処理について質問する問題を開きましたwcscolumns($'My sign is\t鼠鼠')更新: Ericは、wcsexpand()TABをスペースに拡張する機能を追加しました。

$ cat >wcsexpand.awk 
{ printf "%d\n", wcscolumns( wcsexpand($0, 8) ) }
$ awk -f wcwidth.awk -f wcsexpand.awk <<< $'My sign is\t鼠鼠'
20
$ echo $'鼠\tone\n鼠鼠\ttwo'
      one
鼠鼠    two
$ awk -f wcwidth.awk -f wcsexpand.awk <<< $'鼠\tone\n鼠鼠\ttwo'
11
11

1

を使用colしてksh93、私の質問で可能な解決策のヒントを展開するには:

Debian のcolfrom 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' "$@"
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.