문자열의 표시 너비를 가져옵니다.


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

하나는 사용할 수 ksh93printf '%<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는이 기능에 대한 설명에서이 측면을 언급하지 않습니다.
  • 단일 너비로 부여 될 수있는 일부 문자 (예 : 선 그리기)는 (유니 코드로) "모호한 너비"로, wcswidth단독 사용을 통한 응용 프로그램의 이식성을 손상시킵니다 (예 : 2 장 Cygwin 설정 참조 ). xterm예를 들어, 필요한 구성에 대해 2 바이트 문자를 선택할 수있는 기능이 있습니다.
  • 인쇄 가능한 문자 이외의 문자를 처리하려면 시뮬레이션하려는 경우가 아니라면 터미널 에뮬레이터를 사용해야합니다.

셸 API 호출 wcswidth은 다음과 같이 다양하게 지원됩니다.

그것들은 다소 직접적입니다 : wcswidthPerl의 경우 시뮬레이션 하고 Ruby와 Python에서 C 런타임을 호출합니다. 파이썬과 같은 저주를 사용할 수도 있습니다 (문자 결합을 처리 할 것입니다).

  • setupterm을 사용하여 터미널을 초기화합니다 (화면에 텍스트가 기록되지 않음)
  • filter기능 사용 (한 줄)
  • 로 시작 부분에 텍스트를 그려 addstr오류를 확인하고 (너무 긴 경우) 끝 위치를 확인합니다.
  • 공간이 있으면 시작 위치를 조정하십시오.
  • 전화 endwin(하지 않아야합니다 refresh)
  • 시작 위치에 대한 결과 정보를 표준 출력에 기록

정보를 스크립트로 다시 보내거나 직접 호출하는 대신 출력에 curses를 사용 tput하면 전체 행 filter이 지워집니다 ( 한 행으로 제한 함).


나는 이것이 유일한 길이어야한다고 생각한다. 터미널이 2 배 문자를 지원하지 않으면, wcswidth()아무 것도 말하지 않아도 됩니다.
mikeserv

실제로이 방법으로 내가 가진 유일한 문제는 제어 시퀀스에 응답하지 않더라도 plink설정 TERM=xterm됩니다. 그러나 나는 매우 이국적인 터미널을 사용하지 않습니다.
Gilles 'SO- 악마 그만해

감사. 그러나 아이디어는 터미널에 문자열을 표시 하기 전에 해당 정보를 얻는 것이 었습니다. 어디에 표시 할 것인지 알기 위해 터미널 오른쪽에 문자열을 표시하는 최근 질문에 대한 후속 조치 일 것입니다. 내 실제 질문은 실제로 쉘에서 wcswidth에 도달하는 방법에 관한 것이 었습니다). @ mikeserv, yes wcswidth ()는 특정 터미널이 특정 문자열을 표시하는 방법에 대해 잘못되었을 수 있지만 터미널 독립 솔루션에 접근 할 수있는 것과 비슷하며 col / ksh-printf가 내 시스템에서 사용하는 것과 비슷합니다.
Stéphane Chazelas

내가 그 알고 있어요,하지만 덜 휴대용 기능을 통해 제외하고 직접 액세스 할 수없는 wcswidth (당신은 몇 가지 가정을 만들어, 펄에서이 작업을 수행 할 수 - 참조 search.cpan.org/dist/Text-CharWidth/CharWidth.pm를 ) . 그런데 오른쪽 정렬 질문은 문자열을 왼쪽 아래에 쓴 다음 커서 위치와 삽입 컨트롤을 사용하여 오른쪽 아래로 이동하여 향상시킬 수 있습니다.
토마스 디키

1
@ StéphaneChazelas- fold멀티 바이트 및 확장 너비 문자 를 처리하도록 지정 되었습니다 . 백 스페이스를 처리하는 방법은 다음과 같습니다. 줄 폭의 현재 개수는 1 씩 감소하지만 카운트는 음수가되지 않습니다. 다음 문자의 너비가 1보다 커서 행 너비가 너비를 초과 하지 않는 한 접기 유틸리티는 <backspace> 바로 앞이나 뒤에 <newline>을 삽입해서는 안됩니다 . 아마 fold -w[num]pr +[num]어떻게 든 협력 할 수 있을까?
mikeserv

5

한 줄짜리 문자열의 경우 GNU 구현 에는 원하는 것을 정확하게 수행 wc하는 -L(일명 --max-line-length) 옵션이 있습니다 (제어 문자 제외).


1
감사. 디스플레이 너비를 반환 할 줄 몰랐습니다. FreeBSD 구현에도 -L 옵션이 있으며, 의사는 가장 긴 줄의 문자 수를 반환한다고 말하지만 테스트 결과 바이트 수를 나타냅니다 (어쨌든 표시 너비가 아님). OS / X에는 FreeBSD에서 파생 될 것으로 예상했지만 -L이 없습니다.
Stéphane Chazelas

또한 처리 tab하는 것처럼 보입니다 (8 열마다 탭이 중지된다고 가정).
Stéphane Chazelas

실제로, 한 줄 이상의 문자열의 경우 LF 제어 문자를 올바르게 처리하므로 원하는 것을 정확하게 수행한다고 말합니다 .
Stéphane Chazelas

@ StéphaneChazelas : 여전히 문자 수가 아닌 바이트 수를 반환하는 문제가 있습니까? 나는 당신의 데이터에서 그것을 테스트하고 당신이 원하는 결과를 얻었습니다 : wc -L <<< 'unix'→ 8,  wc -L <<< 'Stéphane'→ 8, 그리고  wc -L <<< 'もで 諤奯ゞ'→ 11. PS“Stéphane”은 9 개의 문자로 간주합니다. 그 중 하나는 너비가 0입니까? 그것은 8 문자처럼 보이지만 그중 하나는 멀티 바이트입니다.
G-Man, 'Reinstate

@ G-Man은 FreeBSD 12.0과 UTF-8 로켈이 여전히 바이트를 계산하는 것으로 보이는 FreeBSD 구현을 언급하고있었습니다. é는 하나의 U + 00E9 문자 또는 U + 0065 (e) 문자 다음에 U + 0301 (급성 악센트 조합)을 사용하여 작성할 수 있으며, 후자는 문제에서 보여진 문자입니다.
Stéphane Chazelas 2016 년

4

내에서는 .profile터미널에서 문자열의 너비를 결정하는 스크립트를 호출합니다. 시스템 세트를 신뢰할 수없는 컴퓨터의 콘솔에 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)) " ";끝에 cleanup8 을 추가 하여 사용법을 조금 더 예쁘게 만들었다 . 그것은 또한 아무것도 (A에서 잘되는 라인에 인쇄되어 있지 않은 가정하지만 이것은 테스트가 보이지 않게 ~/.profile)
아담 카츠

사실, zsh을 (5.7.1)에 그냥 할 수있는 약간의 실험에서 나타납니다 text="Éé"다음과 ${#text}당신에게 표시 폭 (I 얻을 줄 것이다 4비 유니 코드 터미널과 2유니 코드 호환 터미널에서). 이것은 bash에는 해당되지 않습니다.
Adam Katz

@AdamKatz ${#text}는 디스플레이 너비를 제공하지 않습니다. 현재 로케일에서 사용하는 인코딩의 문자 수를 제공합니다. 터미널의 인코딩을 결정하고 싶기 때문에 내 목적으로는 쓸모가 없습니다. 다른 이유로 디스플레이 너비를 원하는 경우에 유용하지만 모든 문자가 한 단위 너비 인 것은 아니기 때문에 정확하지 않습니다. 예를 들어 악센트를 결합하면 너비가 0이고 중국어 표의 문자 너비가 2입니다.
Gilles 'SO-stop

그래, 좋은 지적이야 그것은 Stéphane의 질문을 만족시킬 수는 있지만 원래 의도는 아닙니다 (실제로 그렇게하고 싶기 때문에 코드를 조정하는 것입니다). 바라건대 내 첫 번째 의견이 당신에게 도움이 되길 바랍니다. Gilles.
Adam Katz

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

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

colksh93내 질문에 사용 가능한 솔루션에 대한 힌트를 확장하려면 :

컨트롤이 아닌 단일 문자의 너비를 얻으려면 데비안 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"
}

사용 ksh93printf '%Ls':

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

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

사용 perlText::CharWidth:

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.