Получить ширину отображения строки символов

15

Что было бы наиболее близко к переносимому способу получения ширины дисплея (по крайней мере, на терминале (тот, который отображает символы в текущей локали с правильной шириной)) строки символов из сценария оболочки.

В первую очередь меня интересует ширина неконтролирующих символов, но приветствуются также решения, учитывающие управляющие символы, такие как возврат, возврат каретки, горизонтальная табуляция.

Другими словами, я ищу для оболочки API вокруг wcswidth()функции POSIX.

Эта команда должна вернуть:

$ 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

Можно использовать ksh93s, printf '%<n>Ls'который учитывает ширину символов для заполнения <n>столбцов, или colкоманду (например, с printf '++%s\b\b--\n' <character> | col -b), чтобы попытаться получить это, perlпо крайней мере, есть модуль Text :: CharWidth , но есть более прямые или переносимые подходы.

Это более или менее продолжение этого другого вопроса, касающегося отображения текста в правой части экрана, для которого вам потребуется эта информация перед отображением текста.

Стефан Шазелас
источник
Соответствует: eev.ee/blog/2015/09/12/dark-corners-of-unicode/…
Стефан

Ответы:

7

В эмуляторе терминала можно использовать отчет о положении курсора, чтобы получить позиции до / после, например, из

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

и узнайте, насколько широко символы напечатаны на терминале. Поскольку это управляющая последовательность ECMA-48 (а также VT100), поддерживаемая практически любым терминалом, который вы, вероятно, будете использовать, она довольно переносима.

Для справки

    CSI Ps n Отчет о состоянии устройства (DSR).
              ...
                Ps = 6 -> Положение курсора отчета (CPR) [строка; столбец].
              Результат - CSI r; с R

В конечном счете, эмулятор терминала определяет ширину печати из-за следующих факторов:

  • Настройки локали влияют на способ форматирования строки, но последовательность байтов, отправляемых на терминал, интерпретируется на основе конфигурации терминала (отмечая, что некоторые люди утверждают, что это должен быть UTF-8, в то время как с другой стороны мобильность была особенность, запрошенная в вопросе).
  • wcswidthодин не говорит о том, как обрабатываются комбинированные символы; POSIX не упоминает этот аспект в описании этой функции.
  • некоторые символы (например, рисование линий), которые можно считать само собой разумеющимися как одинарную ширину, являются (в Юникоде) «неоднозначной шириной», что подрывает мобильность приложения, использующего wcswidthодин (см., например, главу 2. Настройка Cygwin ). xtermнапример, есть положение для выбора символов двойной ширины для конфигураций, необходимых для этого.
  • для обработки чего-либо, кроме печатаемых символов, вы должны полагаться на эмулятор терминала (если вы не хотите имитировать это).

Вызов API оболочки wcswidthподдерживается в различной степени:

Они более или менее прямые: симуляция wcswidthв случае Perl, вызов C времени выполнения из Ruby и Python. Вы даже можете использовать проклятия, например, из Python (которые будут обрабатывать комбинированные символы):

  • инициализировать терминал с помощью setupterm (текст не выводится на экран)
  • использовать filterфункцию (для отдельных строк)
  • нарисуйте текст в начале строки addstr, проверяя на наличие ошибок (если она слишком длинная), а затем на конечную позицию
  • если есть место, отрегулируйте исходное положение.
  • вызов endwin(который не должен делать refresh)
  • записать полученную информацию о начальной позиции в стандартный вывод

Использование проклятий для вывода (вместо подачи информации обратно в скрипт или прямого вызова tput) очистит всю строку ( filterограничивает ее строкой).

Томас Дики
источник
я думаю, что это должен быть единственный путь, правда. если терминал не поддерживает символы двойной ширины, то не имеет большого значения, что wcswidth()вообще можно сказать о чем-либо.
mikeserv
На практике единственная проблема, с которой я столкнулся при использовании этого метода, заключается в том plink, что он устанавливается, TERM=xtermдаже если он не реагирует ни на одну последовательность управления. Но я не использую очень экзотические терминалы.
Жиль "ТАК - перестань быть злым"
Благодарю. но идея заключалась в том, чтобы получить эту информацию до отображения строки на терминале (чтобы знать, где ее отображать, это продолжение недавнего вопроса об отображении строки справа от терминала, возможно, я должен был упомянуть, что хотя мой настоящий вопрос был действительно о том, как добраться до wcswidth из оболочки). @mikeserv, да wcswidth () может ошибаться из-за того, как конкретный терминал будет отображать конкретную строку, но это настолько близко, насколько вы можете добраться до независимого от терминала решения, и это то, что col / ksh-printf использует в моей системе.
Стефан Шазелас
Я знаю об этом, но wcswidth не доступен напрямую, кроме как через менее переносимые функции (вы можете сделать это в perl, сделав некоторые предположения - см. Search.cpan.org/dist/Text-CharWidth/CharWidth.pm ) , Кстати, вопрос с выравниванием по праву можно (возможно) улучшить, записав строку в нижний левый угол, а затем с помощью элементов управления курсором и вставки поместите ее в правый нижний угол.
Томас Дики
1
@ StéphaneChazelas - foldвидимо, предназначен для работы с многобайтовыми символами и символами расширенной ширины . Вот как он должен обрабатывать возврат: текущий счетчик ширины линии должен быть уменьшен на единицу, хотя счетчик никогда не станет отрицательным. Утилита fold не должна вставлять <newline> непосредственно перед или после любого <backspace>, если только следующий символ не имеет ширину больше 1 и не приведет к тому, что ширина строки превысит ширину. может быть fold -w[num]и pr +[num]можно было как-то объединиться?
mikeserv
5

Для однострочных строк в реализации GNU wcесть опция -L(aka --max-line-length), которая делает именно то, что вы ищете (кроме символов управления).

Эгмонт
источник
1
Благодарю. Я понятия не имел, что он вернет ширину дисплея. Обратите внимание, что реализация FreeBSD также имеет опцию -L, в документе говорится, что она возвращает количество символов в самой длинной строке, но мой тест, похоже, показывает, что это число байтов (а не ширина дисплея в любом случае). В OS / X нет -L, хотя я и ожидал, что он получен из FreeBSD.
Стефан Шазелас
Похоже, что обрабатывать tabтакже (предполагает табуляцию каждые 8 ​​столбцов).
Стефан Шазелас
На самом деле, для строк больше чем в одну строку, я бы сказал, что он также делает именно то, что я ищу, так как в нем правильно обрабатываются управляющие символы LF .
Стефан Шазелас
@ StéphaneChazelas: У вас все еще есть проблема, что это возвращает количество байтов, а не количество символов? Я проверил это на ваших данных и получил желаемые результаты: wc -L <<< 'unix'→ 8,  wc -L <<< 'Stéphane'→ 8 и  wc -L <<< 'もで 諤奯ゞ'→ 11. PS. Вы считаете, что «Стефан» - это девять символов, один из которых имеет нулевую ширину? Это выглядит как восемь символов, один из которых многобайтовый.
G-Man говорит: «Восстановите Монику»
@ G-Man, я ссылался на реализацию FreeBSD, которая во FreeBSD 12.0 и локали UTF-8, похоже, все еще считает байты. Обратите внимание, что é может быть написано с использованием одного символа U + 00E9 или символа U + 0065 (e), за которым следует U + 0301 (сочетая острый акцент), последний из которых показан в вопросе.
Стефан
4

По моему .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
# /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 произвольно; оно должно быть достаточно длинным, чтобы охватить более широкий вывод старых локалей, но достаточно узким, чтобы избежать переноса строк). Это делает тест невидимым, хотя и предполагает, что в строке ничего не напечатано (что хорошо в a ~/.profile)
Адам Кац
На самом деле, из небольшого эксперимента видно, что в zsh (5.7.1) вы можете просто сделать, text="Éé"а затем ${#text}дать вам ширину дисплея (я получаю 4в терминале, не 2поддерживающем юникод, и в терминале, совместимом с юникодом). Это не верно для Баш.
Адам Кац
@AdamKatz ${#text}не дает вам ширину экрана. Он дает вам количество символов в кодировке, используемой текущей локалью. Что бесполезно для моей цели, так как я хочу определить кодировку терминала. Это полезно, если вам нужна ширина экрана по какой-то другой причине, но она не точна, потому что не каждый символ имеет ширину в одну единицу. Например, объединяющие акценты имеют ширину 0, а китайские идеограммы имеют ширину 2.
Жиль "ТАК - перестать быть злым"
Да, хорошая мысль. Это может удовлетворить вопрос Стефана, но не ваше первоначальное намерение (что на самом деле я тоже хотел сделать, тем самым адаптировав ваш код). Надеюсь, мой первый комментарий был вам полезен, Жиль.
Адам Кац
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, поскольку их wcscolumns($'My sign is\t鼠鼠')должно быть больше 14. Обновление: Эрик добавил функцию 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
xebeche
источник
1

Чтобы расширить намеки на возможные решения, используя colи ksh93в моем вопросе:

Использование colfrom bsdmainutilsв Debian (может не работать с другими 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's printf '%Ls':

charwidth() {
  set "$(printf '.%2Ls.' "$1")"
  echo "$((5 - ${#1}))"
}

stringwidth() {
  set "$(printf '.%*Ls.' "$((2*${#1}))" "$1")" "$1"
  echo "$((2 + 3 * ${#2} - ${#1}))"
}

Используя perl's Text::CharWidth:

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
Стефан Шазелас
источник