쉘 스크립트(적어도 터미널(문자가 현재 로케일에서 올바른 너비로 표시되는 터미널))에서 문자열의 표시 너비를 얻는 가장 가까운 휴대용 방법은 무엇입니까?
주로 비제어 문자의 너비에 관심이 있지만 백스페이스, 캐리지 리턴, 가로 탭 등과 같은 제어 문자에 대한 솔루션도 환영합니다.
즉, 나는 다음을 찾고 있습니다.껍데기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
하거나 예를 들어 최소한 Text::CharWidth 모듈이 있지만 더 직접적이거나 더 이식 가능한 방법이 있습니까? ?printf '%<n>Ls'
<n>
col
printf '++%s\b\b--\n' <character> | col -b
perl
이것은 거의 후속 조치입니다.또 다른 문제이는 화면 오른쪽에 텍스트를 표시하는 것에 관한 것이므로 텍스트를 표시하기 전에 해당 정보를 가져와야 합니다.
답변1
한 줄 문자열의 경우 GNU 구현에는 필요한 작업을 정확하게 수행하는 (제어 문자 제외) 옵션이 있습니다 wc
.-L
--max-line-length
답변2
터미널 에뮬레이터에서는 커서 위치 보고를 사용하여 이전/이후 위치를 가져올 수 있습니다.
...record position
printf '%s' $string
...record position
터미널에 인쇄된 문자의 너비가 얼마나 되는지 알아보세요. 이는 ECMA-48(및 VT100) 제어 시퀀스이므로 사용하는 거의 모든 터미널이 이를 지원하므로 이식성이 뛰어납니다.
참고로
CSI Ps n 장치 상태 보고서(DSR). ... Ps = 6 -> 커서 위치(CPR) [행;열] 보고. 결과는 CSI r 수용체입니다.
궁극적으로 터미널 에뮬레이터는 다음 요소로 인해 인쇄 가능한 너비를 결정합니다.
- 로케일은 문자열 형식에 영향을 주지만 터미널로 전송된 일련의 바이트는 터미널 구성 방식에 따라 해석됩니다(어떤 사람들은 UTF-8이어야 한다고 생각할 수도 있지만 반면에이식성질문에서 요청한 기능입니다).
wcswidth
POSIX만으로는 함수 설명에서 이러한 측면을 언급하지 않습니다.- 당연하다고 생각할 수 있는 일부 단일 너비 문자(예: 선 그리기)는 (유니코드에서) "모호한 너비"로,
wcswidth
독립 실행형 응용 프로그램의 이식성을 파괴합니다(예:2장 Cygwin 설정).xterm
예를 들어, 원하는 구성에 대해 2바이트 문자를 선택할 수 있습니다. - 인쇄 가능한 문자 이외의 것을 처리하려면 터미널 에뮬레이터를 사용해야 합니다(에뮬레이션하려는 경우 제외).
wcswidth
Shell API 호출은 다양한 수준으로 지원됩니다.
- Text::CharWidth - 터미널에서 문자열이 차지하는 열 수를 가져옵니다.
이 모듈은 기능을 제공합니다비슷한예를 들어 C 언어의 wcwidth(3) 및 wcswidth(3)입니다.
- 논의하다루비용
- 파이썬 API
이는 다소 간단합니다. wcswidth
Perl의 경우 에뮬레이션, Ruby 및 Python에서 C 런타임 호출. Python에서처럼 (문자 결합을 처리할 수 있는) 저주를 사용할 수도 있습니다.
- 초기화 터미널 사용설정 항목(화면에 텍스트가 기록되지 않습니다)
- 사용
filter
기능(단일 행용) - 줄의 시작 부분에 텍스트 그리기
addstr
, 오류가 있는지 확인하고(너무 긴 경우) 끝 위치를 확인하세요. - 공간이 있으면 시작 위치를 조정하십시오.
- 부르다
endwin
(이렇게 하면 안 된다.refresh
) - 시작 위치에 대한 결과 정보를 표준 출력에 씁니다.
저주를 사용하다산출(정보를 스크립트에 다시 공급하거나 직접 호출하는 대신 tput
) 전체 줄을 지웁니다( filter
실제로 한 줄로 제한).
답변3
내에서는 .profile
터미널의 문자열 너비를 결정하는 스크립트를 호출합니다. system-set 을 신뢰하지 않는 머신 콘솔에 로그인했을 때 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
# https://unix.stackexchange.com/questions/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
답변4
내 문제에서 및 를 사용하여 col
가능한 솔루션을 확장하는 팁 :ksh93
제어가 아닌 단일 문자의 너비를 얻으려면 데비안에서 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"
}
사용 :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' -- "$@"
}