Отримайте ширину відображення рядка символів


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

Можна використати ksh93's, printf '%<n>Ls'яка враховує ширину символів для вкладки в <n>стовпці, або colкоманду (наприклад, printf '++%s\b\b--\n' <character> | col -bдля того, щоб спробувати це зробити, є текст :: CharWidthperl принаймні є модуль , але є більш прямі або портативні підходи.

Це більш-менш подальше запитання щодо іншого питання, яке стосувалося відображення тексту в правій частині екрана, для якого вам потрібно мати цю інформацію, перш ніж відображати текст.


Відповіді:


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 Shell wcswidthпідтримуються в різній мірі:

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

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

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


Я думаю, що це повинен бути єдиний спосіб. якщо термінал не підтримує символів подвійної ширини, то він не має великого значення, що взагалі нічого не wcswidth()має говорити.
mikeserv

На практиці єдина проблема, яку я мав з цим методом plink, яка встановлюється, TERM=xtermхоча він не відповідає жодній контрольній послідовності. Але я не використовую дуже екзотичні термінали.
Жил 'SO- перестань бути злим'

Спасибі. але ідея полягала в тому, щоб отримати цю інформацію перед тим, як відображати рядок у терміналі (щоб знати, де її відображати, це подальше запитання щодо останнього питання про відображення рядка праворуч від терміналу, можливо, я мав би сказати, що хоча моє справжнє запитання було насправді про те, як дістатися до wcswidth з оболонки). @mikeserv, так wcswidth () може бути помилковим у тому, як конкретний термінал відображатиме певну рядок, але це максимально близько, як ви можете дістатись до незалежного від терміналу рішення, і саме це використовує col / ksh-printf у моїй системі.
Stéphane Chazelas

Я знаю про це, але wcswidth не доступний безпосередньо, за винятком менш портативних функцій (ви можете зробити це в perl, зробивши деякі припущення - див. Search.cpan.org/dist/Text-CharWidth/CharWidth.pm ) . Питання щодо вирівнювання правого способу можна, можливо, покращити, записавши рядок вліво ліворуч, а потім за допомогою позиції курсору та елементів вставки, щоб перемістити його в нижній правий.
Томас Дікі

1
@ StéphaneChazelas - 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 Ви вважаєте, що "Stéphane" є дев'ятьма символами, один з яких дорівнює нулю ширини? На мене це схоже на вісім символів, один з яких багатобайтовий.
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, що отримує доступ до віддалених файлів з машини Linux за допомогою plinkметоду, і я вирішив його, використовуючи 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 термінал і 2в термінал, сумісний з unicode). Це не вірно для баш.
Адам Кац

@AdamKatz ${#text}не дає вам ширину відображення. Він дає вам кількість символів у кодуванні, використовуваному поточним мовою. Що марно для моєї мети, оскільки я хочу визначити кодування терміналу. Це корисно, якщо ви хочете ширину відображення з якоїсь іншої причини, але це не точно, оскільки не кожен символ шириною однієї одиниці. Наприклад, комбінуючі наголоси мають ширину 0, а китайські ідеограми шириною 2.
Жил "SO - перестань бути злим"

Так, хороший момент. Це може задовольнити питання Стефана, але не ваше первісне задум (саме це я і хотів зробити, таким чином, адаптувавши ваш код). Сподіваюся, мій перший коментар був корисним для вас, Жилле.
Адам Кац

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

Я відкрив питання з проханням про звернення з ТКС , так як wcscolumns($'My sign is\t鼠鼠')має бути більше , ніж 14 Update: Ерік додав функцію wcsexpand()для розширення ТКС просторів:

$ 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в моєму питанні:

Використовуючи colвід bsdmainutilsDebian (може не працювати з іншими 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' "$@"
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.