Lấy chiều rộng hiển thị của một chuỗi ký tự


15

Điều gì sẽ là gần nhất với một cách di động để có được chiều rộng hiển thị (trên một thiết bị đầu cuối ít nhất (một cách hiển thị các ký tự trong ngôn ngữ hiện tại với độ rộng chính xác)) của một chuỗi các ký tự từ tập lệnh shell.

Tôi chủ yếu quan tâm đến độ rộng của các ký tự không điều khiển nhưng các giải pháp có tính đến các ký tự điều khiển như backspace, trả về vận chuyển, lập bảng ngang cũng được chào đón.

Nói cách khác, tôi đang tìm kiếm một API shell xung quanh wcswidth()chức năng POSIX.

Lệnh đó sẽ trả về:

$ 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

Người ta có thể sử dụng ksh93's printf '%<n>Ls'mà sẽ đưa vào tài khoản của độ rộng ký tự cho padding để <n>cột, hoặc các collệnh (với ví dụ printf '++%s\b\b--\n' <character> | col -b) để thử và lấy được rằng, có một chữ :: CharWidth perlmô-đun ít nhất, nhưng có cách tiếp cận trực tiếp hơn hoặc cầm tay.

Đó ít nhiều là một câu hỏi tiếp theo cho câu hỏi khác về việc hiển thị văn bản ở bên phải màn hình mà bạn sẽ cần phải có thông tin đó trước khi hiển thị văn bản.


Câu trả lời:


7

Trong trình giả lập thiết bị đầu cuối, người ta có thể sử dụng báo cáo vị trí con trỏ để nhận các vị trí trước / sau, ví dụ: từ

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

và tìm độ rộng của các ký tự được in trên thiết bị đầu cuối. Vì đó là chuỗi điều khiển ECMA-48 (cũng như VT100) được hỗ trợ bởi hầu hết mọi thiết bị đầu cuối mà bạn có khả năng sử dụng, nên nó khá dễ mang theo.

Để tham khảo

    Báo cáo trạng thái thiết bị CSI Ps n (DSR).
              ...
                Ps = 6 -> Báo cáo vị trí con trỏ (CPR) [hàng; cột].
              Kết quả là CSI r; c

Cuối cùng, trình giả lập thiết bị đầu cuối xác định chiều rộng có thể in được, vì các yếu tố sau:

  • cài đặt ngôn ngữ ảnh hưởng đến cách một chuỗi có thể được định dạng, nhưng chuỗi byte được gửi đến thiết bị đầu cuối được diễn giải dựa trên cách cấu hình của thiết bị đầu cuối (lưu ý rằng một số người sẽ cho rằng nó phải là UTF-8, mặt khác tính di động là tính năng được yêu cầu trong câu hỏi).
  • wcswidthmột mình không cho biết cách kết hợp các ký tự được xử lý; POSIX không đề cập đến khía cạnh này trong phần mô tả chức năng đó.
  • một số ký tự (ví dụ vẽ đường thẳng) mà người ta có thể coi là độ rộng đơn là (theo Unicode) "chiều rộng mơ hồ", làm suy yếu tính di động của một ứng dụng wcswidthchỉ sử dụng (ví dụ Chương 2. Thiết lập Cygwin ). xtermví dụ, có điều khoản để chọn các ký tự có chiều rộng gấp đôi cho các cấu hình cần thiết này.
  • để xử lý bất cứ thứ gì ngoài các ký tự có thể in, bạn sẽ phải dựa vào trình giả lập thiết bị đầu cuối (trừ khi bạn muốn mô phỏng điều đó).

Gọi API Shell wcswidthđược hỗ trợ ở các mức độ khác nhau:

Đó là trực tiếp ít nhiều: mô phỏng wcswidthtrong trường hợp của Perl, gọi thời gian chạy C từ Ruby và Python. Bạn thậm chí có thể sử dụng các lời nguyền, ví dụ, từ Python (sẽ xử lý kết hợp các ký tự):

  • khởi tạo thiết bị đầu cuối bằng cách sử dụng setupterm (không có văn bản nào được ghi lên màn hình)
  • sử dụng filterhàm (cho các dòng đơn)
  • vẽ văn bản ở đầu dòng với addstr, kiểm tra lỗi (trong trường hợp quá dài), và sau đó cho vị trí kết thúc
  • nếu có phòng, điều chỉnh vị trí bắt đầu.
  • gọi endwin(không nên làm a refresh)
  • ghi thông tin kết quả về vị trí bắt đầu vào đầu ra tiêu chuẩn

Sử dụng các lời nguyền cho đầu ra (thay vì đưa thông tin trở lại tập lệnh hoặc gọi trực tiếp tput) sẽ xóa toàn bộ dòng ( filterkhông giới hạn nó trong một dòng).


Tôi nghĩ rằng đây phải là cách duy nhất, thực sự. nếu thiết bị đầu cuối không hỗ trợ ký tự hai chiều rộng, thì nó không có vấn đề gì wcswidth()để nói về bất cứ điều gì cả.
mikeerv

Trong thực tế, vấn đề duy nhất tôi gặp phải với phương pháp này là plink, nó đặt ra TERM=xtermmặc dù nó không đáp ứng với bất kỳ chuỗi điều khiển nào. Nhưng tôi không sử dụng thiết bị đầu cuối rất kỳ lạ.
Gilles 'SO- ngừng trở nên xấu xa'

Cảm ơn. nhưng ý tưởng là lấy thông tin đó trước khi hiển thị chuỗi trên thiết bị đầu cuối (để biết nơi hiển thị nó, đó là câu hỏi tiếp theo về việc hiển thị chuỗi bên phải của thiết bị đầu cuối, có lẽ tôi nên đề cập rằng mặc dù câu hỏi thực sự của tôi là về cách lấy băng thông từ trình bao). @mikeerv, có wcsference () có thể sai về cách một thiết bị đầu cuối cụ thể sẽ hiển thị một chuỗi cụ thể, nhưng nó gần đến mức bạn có thể sử dụng giải pháp độc lập với thiết bị đầu cuối và đó là cách sử dụng col / ksh-printf trên hệ thống của tôi.
Stéphane Chazelas

Tôi biết điều đó, nhưng băng thông không thể truy cập trực tiếp ngoại trừ thông qua các tính năng ít di động (bạn có thể thực hiện việc này trong perl, bằng cách đưa ra một số giả định - xem search.cpan.org/dist/Text-CharWidth/CharWidth.pm ) . Bằng cách này, câu hỏi căn lề phải có thể được cải thiện bằng cách viết chuỗi sang phía dưới bên trái và sau đó sử dụng vị trí con trỏ và điều khiển chèn để chuyển nó sang phía dưới bên phải.
Thomas Dickey

1
@ StéphaneChazelas - foldrõ ràng là sẽ xử lý các ký tự có nhiều byte và chiều rộng mở rộng . Đây là cách nó nên xử lý backspace: Số lượng chiều rộng dòng hiện tại sẽ bị giảm đi bởi một, mặc dù số lượng không bao giờ trở thành âm. Tiện ích gấp không được chèn <newline> ngay trước hoặc sau bất kỳ <backspace> nào, trừ khi ký tự sau có chiều rộng lớn hơn 1 và sẽ khiến chiều rộng của dòng vượt quá chiều rộng. có lẽ fold -w[num]pr +[num]có thể được hợp tác bằng cách nào đó?
mikeerv

5

Đối với các chuỗi một dòng, việc triển khai GNU wccó tùy chọn -L(aka --max-line-length) thực hiện chính xác những gì bạn đang tìm kiếm (ngoại trừ các ký tự điều khiển).


1
Cảm ơn. Tôi không biết nó sẽ trả về chiều rộng màn hình. Lưu ý rằng việc triển khai FreeBSD cũng có tùy chọn -L, tài liệu nói rằng nó trả về số lượng ký tự trong dòng dài nhất, nhưng thử nghiệm của tôi dường như chỉ ra đó là một số byte thay vào đó (không phải chiều rộng hiển thị trong bất kỳ chữ nào). OS / X không có -L mặc dù tôi đã mong đợi nó xuất phát từ FreeBSD.
Stéphane Chazelas

Nó dường tabnhư cũng xử lý (giả sử tab dừng mỗi 8 cột).
Stéphane Chazelas

Trên thực tế, đối với các chuỗi nhiều hơn một dòng, tôi sẽ nói rằng nó cũng thực hiện chính xác những gì tôi đang tìm kiếm, vì trong đó xử lý các ký tự điều khiển LF đúng cách .
Stéphane Chazelas

@ StéphaneChazelas: Bạn vẫn gặp vấn đề là điều này trả về số byte chứ không phải số lượng ký tự? Tôi đã kiểm tra dữ liệu trên dữ liệu của bạn và nhận được kết quả bạn muốn: wc -L <<< 'unix'→ 8,  wc -L <<< 'Stéphane'→ 8 và  wc -L <<< 'もで 諤奯ゞ'→ 11. Bạn có nghĩ rằng St Sthhane Đây là chín ký tự, một trong số đó có độ rộng bằng không? Nó trông giống như tám ký tự, một trong số đó là nhiều byte.
G-Man nói 'Phục hồi Monica'

@ G-Man, tôi đã đề cập đến việc triển khai FreeBSD, trong FreeBSD 12.0 và ngôn ngữ UTF-8 dường như vẫn đang đếm byte. Lưu ý rằng é có thể được viết bằng một ký tự U + 00E9 hoặc ký tự U + 0065 (e) theo sau là U + 0301 (kết hợp dấu trọng âm), ký tự sau là ký tự hiển thị trong câu hỏi.
Stéphane Chazelas

4

Theo tôi .profile, tôi gọi một tập lệnh để xác định độ rộng của chuỗi trên thiết bị đầu cuối. Tôi sử dụng điều này khi đăng nhập vào bảng điều khiển của máy mà tôi không tin tưởng vào hệ thống được đặt LC_CTYPEhoặc khi tôi đăng nhập từ xa và không thể tin tưởngLC_CTYPE để khớp với phía từ xa. Kịch bản của tôi truy vấn thiết bị đầu cuối, thay vì gọi bất kỳ thư viện nào, vì đó là toàn bộ điểm trong trường hợp sử dụng của tôi: xác định mã hóa của thiết bị đầu cuối.

Điều này là mong manh theo nhiều cách:

  • nó sửa đổi màn hình, vì vậy nó không phải là trải nghiệm người dùng tốt đẹp;
  • có một điều kiện cuộc đua nếu một chương trình khác hiển thị một cái gì đó không đúng lúc;
  • nó khóa nếu thiết bị đầu cuối không đáp ứng. (Một vài năm trước tôi đã hỏi làm thế nào để cải thiện vấn đề này , nhưng nó không phải là vấn đề trong thực tế nên tôi chưa bao giờ chuyển sang giải pháp đó. Windows Emacs truy cập các tệp từ xa từ máy Linux plinkvà tôi đã giải quyết bằng cách sử dụng plinkxphương thức này .)

Điều này có thể hoặc có thể không phù hợp với trường hợp sử dụng của bạn.

#! /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

Tập lệnh trả về chiều rộng trong trạng thái trả về của nó, được cắt thành 100. Sử dụng mẫu:

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

Điều này rất hữu ích với tôi (mặc dù tôi chủ yếu sử dụng phiên bản cô đọng của bạn ). Tôi đã sử dụng nó tốt hơn một chút bằng cách thêm printf "\r%*s\r" $((${#text}+8)) " ";vào cuối cleanup(thêm 8 là tùy ý; nó cần đủ dài để bao phủ đầu ra rộng hơn của các địa phương cũ nhưng đủ hẹp để tránh bị quấn dòng). Điều này làm cho bài kiểm tra trở nên vô hình, mặc dù nó cũng cho rằng không có gì được in trên dòng (điều này cũng ổn trong một ~/.profile)
Adam Katz

Trên thực tế, nó xuất hiện từ một thử nghiệm nhỏ mà trong zsh (5.7.1) bạn chỉ có thể làm text="Éé"và sau đó ${#text}sẽ cung cấp cho bạn chiều rộng hiển thị (tôi nhận được 4trong một thiết bị đầu cuối không unicode và 2trong một thiết bị đầu cuối tuân thủ unicode). Điều này không đúng với bash.
Adam Katz

@AdamKatz ${#text}không cung cấp cho bạn chiều rộng màn hình. Nó cung cấp cho bạn số lượng ký tự trong mã hóa được sử dụng bởi miền địa phương hiện tại. Điều này là vô ích cho mục đích của tôi vì tôi muốn xác định mã hóa của thiết bị đầu cuối. Nó rất hữu ích nếu bạn muốn chiều rộng màn hình vì một số lý do khác, nhưng nó không chính xác vì không phải mọi ký tự đều rộng một đơn vị. Ví dụ: kết hợp các dấu có chiều rộng bằng 0 và các chữ tượng hình Trung Quốc có chiều rộng là 2.
Gilles 'SO- ngừng là ác'

Vâng, điểm tốt. Nó có thể đáp ứng câu hỏi của Stéphane nhưng không phải là mục đích ban đầu của bạn (đó thực sự là điều tôi muốn làm, do đó tôi điều chỉnh mã của bạn). Hy vọng rằng bình luận đầu tiên của tôi là hữu ích cho bạn, Gilles.
Adam Katz

3

Eric Pruitt đã viết một triển khai ấn tượng wcwidth()wcswidth()trong Awk có sẵn tại wc rắc.awk . Nó chủ yếu cung cấp 4 chức năng

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

nơi wcscolumns()cũng chấp nhận các ký tự không in được.

$ 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

Tôi đã mở một vấn đề hỏi về việc xử lý TAB vì wcscolumns($'My sign is\t鼠鼠')phải lớn hơn 14. Cập nhật: Eric đã thêm chức năng wcsexpand()mở rộng TAB vào không gian:

$ 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

Để mở rộng các gợi ý về các giải pháp có thể sử dụng colksh93trong câu hỏi của tôi:

Sử dụng coltừ bsdmainutilstrên Debian (có thể không hoạt động với các coltriển khai khác ), để có được độ rộng của một ký tự không điều khiển:

charwidth() {
  set "$(printf '...%s\b\b...\n' "$1" | col -b)"
  echo "$((${#1} - 4))"
}

Thí dụ:

$ charwidth x
1
$ charwidth $'\u301'
0
$ charwidth $'\u94f6'
2

Mở rộng cho một chuỗi:

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"
}

Sử dụng ksh93printf '%Ls':

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

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

Sử dụng perlText::CharWidth:

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.