Bir karakter dizisinin görüntüleme genişliğini alma


15

Bir kabuk komut dosyasından bir karakter dizesinin görüntüleme genişliğini (en azından bir terminalde (geçerli yereldeki karakterleri doğru genişlikte gösteren)) elde etmek için taşınabilir bir yola en yakın olanıdır.

Öncelikle kontrol dışı karakterlerin genişliği ile ilgileniyorum, ancak backspace, satırbaşı, yatay tablolama gibi kontrol karakterlerini dikkate alan çözümler de hoş geldiniz.

Başka bir deyişle, POSIX işlevi etrafında bir kabuk API arıyorum wcswidth().

Bu komut geri dönmelidir:

$ 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

Bir kullanabilirsiniz ksh93's printf '%<n>Ls'dikkate için dolgu için karakter genişliği alır <n>sütunları veya col(örneğin ile komutu printf '++%s\b\b--\n' <character> | col -bdenemek ve bu türetmek için), orada :: CharWidth Metin var perlen azından modülü, ama orada daha doğrudan veya taşınabilir yaklaşımlardır.

Bu , ekranın sağında, metni görüntülemeden önce bu bilgilere sahip olmanız gereken başka bir soru hakkında aşağı yukarı bir takiptir.


Yanıtlar:


7

Bir terminal öykünücüsünde, konumlardan önce / sonra almak için imleç konum raporu kullanılabilir, örn.

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

ve karakterlerin uçbirimde ne kadar geniş bastığını bulun. Bu, kullanma olasılığınız olan hemen hemen tüm terminaller tarafından desteklenen bir ECMA-48 (ve VT100) kontrol dizisi olduğundan, oldukça taşınabilirdir.

Referans için

    CSI Ps n Aygıt Durum Raporu (DSR).
              ...
                Ps = 6 -> Rapor İmleç Konumu (CPR) [satır; sütun].
              Sonuç CSI r'dir; c R

Nihayetinde, terminal emülatörü aşağıdaki faktörler nedeniyle yazdırılabilir genişliği belirler:

  • yerel ayarlar, bir dizenin biçimlendirilme şeklini etkiler, ancak terminale gönderilen bayt serisi, terminalin nasıl yapılandırıldığına göre yorumlanır (diğer yandan, bazı kişilerin bunun UTF-8 olması gerektiğini iddia edeceğini belirterek) taşınabilirlik , soruda istenen özellikti).
  • wcswidthtek başına karakterlerin nasıl ele alındığını söylemez; POSIX, bu işlevin açıklamasında bu durumdan bahsetmez.
  • tek genişlik olarak wcswidthverilebilecek bazı karakterler (örneğin çizgi çizimi) (Unicode'da) "belirsiz genişlik" tir ve tek başına kullanılan bir uygulamanın taşınabilirliğini azaltır (örneğin bkz. Bölüm 2. Cygwin Kurulumu ). xtermörneğin buna ihtiyaç duyulan konfigürasyonlar için çift genişlikli karakterleri seçme imkanı vardır.
  • yazdırılabilir karakterler dışında herhangi bir şeyle başa çıkmak için, terminal öykünücüsüne güvenmeniz gerekir (bunu simüle etmek istemiyorsanız).

Arama yapan Shell API'leri wcswidthçeşitli derecelerde desteklenir:

Bunlar az çok doğrudan: wcswidthPerl durumunda simüle ederek Ruby ve Python'dan C çalışma zamanını çağırıyorlar. Hatta Python'dan (karakterleri birleştiren) lanetler bile kullanabilirsiniz:

  • setupterm kullanarak terminali başlatma (ekrana metin )
  • kullan filter (tek hatları için) fonksiyonu
  • metni satırın başına, addstrhata olup olmadığını kontrol edin (çok uzun olması durumunda) ve sonra bitiş konumu için
  • yer varsa, başlangıç ​​konumunu ayarlayın.
  • çağrı endwin(yapmamalı refresh)
  • başlangıç ​​konumu hakkında elde edilen bilgileri standart çıktıya yazın

Çıktı için küfürler kullanmak (bilgiyi bir betiğe geri göndermek veya doğrudan çağırmak yerine tput) tüm satırı temizler ( filterbir satırla sınırlar).


bence bu tek yol olmalı, gerçekten. terminal çift genişlikli karakterleri desteklemiyorsa, wcswidth()hiçbir şey hakkında ne söylenmesi gerektiği önemli değildir .
mikeserv

Uygulamada, bu yöntemle yaşadığım tek sorun , herhangi bir kontrol sekansına cevap vermese bile plinkayarlanan TERM=xterm. Ama çok egzotik terminaller kullanmıyorum.
Gilles 'SO- kötü olmayı kes'

Teşekkürler. ama fikir, terminalde dizeyi görüntülemeden önce bu bilgiyi elde etmekti (nerede görüntüleneceğini bilmek için, terminalin sağında bir dize görüntüleme ile ilgili son sorunun bir takibi, belki de benim gerçek sorum gerçekten wcswidth kabuk almak için ilgili oldu). @mikeserv, yes wcswidth (), belirli bir terminalin belirli bir dizeyi nasıl görüntüleyeceği konusunda yanlış olabilir, ancak terminalden bağımsız bir çözüme ulaşabileceğiniz kadar yakındır ve col / ksh-printf sistemimde bunu kullanır.
Stéphane Chazelas

Bunun farkındayım, ancak wcswidth'e daha az taşınabilir özellikler dışında doğrudan erişilemiyor (bunu bazı varsayımlar yaparak perl'de yapabilirsiniz - bkz. Search.cpan.org/dist/Text-CharWidth/CharWidth.pm ) . Bu arada sağa hizalama sorusu, dizeyi sol alt kısma yazıp ardından imleci konum ve insert-kontrolleri kullanarak sağ alt köşeye kaydırmak suretiyle geliştirilebilir (belki de).
Thomas Dickey

1
@ StéphaneChazelas - foldgörünüşe göre çok baytlı ve genişletilmiş genişlikli karakterlerle başa çıkması bekleniyor . Geri almayı şu şekilde ele alması gerekir: Sayım asla negatif olmayacak olsa da, mevcut çizgi genişliği sayısı bir azaltılacaktır. Katlama yardımcı programı, aşağıdaki karakterin genişliği 1'den büyük olmadığı ve çizgi genişliğinin genişliğini aşmasına neden olmadıkça , herhangi bir <backspace> öğesinden hemen önce veya sonra <newline> eklememelidir . belki fold -w[num]ve bir pr +[num]şekilde bir araya getirilebilir mi?
mikeserv

5

Tek satırlık dizeler için GNU uygulaması wcbir sahiptir -L(aka --max-line-lengthsen (kontrol karakter hariç) için tam olarak aradığınızı yapar) seçeneği.


1
Teşekkürler. Ekran genişliğini döndüreceği konusunda hiçbir fikrim yoktu. FreeBSD uygulamasının da -L seçeneği olduğunu unutmayın, doc en uzun satırdaki karakter sayısını döndürdüğünü söylüyor, ancak testim bunun yerine bir bayt sayısı olduğunu gösteriyor (her durumda ekran genişliği değil). OS / X, FreeBSD'lerden türetmeyi beklememe rağmen -L içermiyor.
Stéphane Chazelas

Aynı şekilde işlenir tabgibi görünüyor (her 8 sütunda sekme durağı olduğu varsayılır).
Stéphane Chazelas

Aslında, bir satırdan fazla dizeler için, tam olarak aradığımı yaptığını söyleyebilirim, çünkü LF kontrol karakterlerini düzgün işliyor .
Stéphane Chazelas

@ StéphaneChazelas: Bunun hala karakter sayısı yerine bayt sayısını döndürmesi sorunu mu yaşıyorsunuz? Verilerinizi test ettim ve istediğiniz sonuçları elde ettim: wc -L <<< 'unix'→ 8,  wc -L <<< 'Stéphane'→ 8 ve  wc -L <<< 'もで 諤奯ゞ'→ 11. PS “Stéphane” nin biri sıfır genişlikli dokuz karakter olduğunu mu düşünüyorsunuz? Bana biri sekiz baytlık sekiz karakter gibi geliyor.
G-Man

@ G-Man, FreeBSD 12.0 ve bir UTF-8 yerel ayarının hala bayt saydığı görünen FreeBSD uygulamasından bahsediyordum. É'nin bir U + 00E9 karakteri veya bir U + 0065 (e) karakteri ve ardından U + 0301 (akut aksanı birleştirerek) kullanılarak yazılabileceğini unutmayın; ikincisi soruda gösterilen karakterdir.
Stéphane Chazelas

4

Benim .profilebir terminalde bir dizenin genişliğini belirlemek için bir komut dosyası çağırıyorum. Sistem setine güvenmediğim bir makinenin konsolunda oturum açarken LC_CTYPEveya uzaktan oturum açtığımda LC_CTYPEve uzak tarafla eşleşmeye güvenemediğimde bunu kullanıyorum. Komut dosyam, herhangi bir kitaplığı çağırmak yerine terminali sorgular, çünkü kullanım durumumdaki bütün nokta buydu: terminalin kodlamasını belirleyin.

Bu çeşitli şekillerde kırılgandır:

  • ekranı değiştirir, bu yüzden çok hoş bir kullanıcı deneyimi değildir;
  • başka bir program yanlış zamanda bir şey gösterirse bir yarış koşulu vardır;
  • terminal yanıt vermezse kilitlenir. (Birkaç yıl önce bunu nasıl geliştireceğimizi sordum , ancak pratikte çok fazla sorun olmadı, bu yüzden bu çözüme geçmeye hiç gelmedim. Yanıt vermeyen bir terminalde karşılaştığım tek durum plinkyöntemle bir Linux makinesinden uzak dosyalara erişen bir Windows Emacs ve bunun yerine yöntemi kullanarak plinkxçözdüm .)

Bu, kullanım durumunuzla eşleşebilir veya eşleşmeyebilir.

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

Komut dosyası genişliği 100'e kırpılmış olarak döndürme durumunda döndürür. Örnek kullanım:

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

(Çoğunlukla kısaltılmış sürümü kullandım ) bu bana yardımcı oldu . printf "\r%*s\r" $((${#text}+8)) " ";Sonuna ekleyerek kullanımını biraz daha güzel yaptım cleanup(8 eklemek keyfi; eski yerlerin daha geniş çıktısını kapsayacak kadar uzun olmalı, ancak bir satır sargısını önlemek için yeterince dar olmalı). Bu, testi görünmez yapar, ancak hatta hiçbir şeyin basılmadığını varsayar (ki bu iyi bir şeydir ~/.profile)
Adam Katz

Aslında, zsh (5.7.1) 'de sadece yapabileceğiniz text="Éé"ve daha sonra ${#text}size ekran genişliğini vereceğiniz küçük bir deneyden anlaşılıyor ( 4unicode olmayan bir terminalde ve 2unicode uyumlu bir terminalde alıyorum ). Bu bash için geçerli değil.
Adam Katz

@AdamKatz ${#text}size ekran genişliğini vermiyor. Geçerli yerel ayar tarafından kullanılan kodlamadaki karakter sayısını verir. Terminalin kodlamasını belirlemek istediğim için amacım işe yaramaz. Ekran genişliğini başka bir nedenle istiyorsanız yararlıdır, ancak her karakter bir birim genişliğinde olmadığı için doğru değildir. Örneğin birleştirme vurgular 0 arasında bir genişliğe sahiptir, ve Çin ideogramların 2. bir genişliğe sahip
Gilles 'SO dur olmak kötü'

Evet, iyi bir noktaya değindin. Stéphane'nin sorusunu tatmin edebilir ama asıl amacınız değil (aslında benim de yapmak istediğim şey, bu yüzden kodumu uyarlamak). Umarım ilk yorumum size yardımcı olmuştur, Gilles.
Adam Katz

3

Eric Pruitt etkileyici bir uygulama yazdım wcwidth()ve wcswidth()mevcut awk içinde wcwidth.awk . Temelde 4 fonksiyon sağlar

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

burada wcscolumns()yazdırılamayan karakterleri de tolere eder.

$ 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'lerin ele alınmasına ilişkin 14'ten büyük olması gereken bir sorun açtım wcscolumns($'My sign is\t鼠鼠'). Güncelleme: Eric, wcsexpand()TAB'leri boşluklara genişletme işlevini ekledi :

$ 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

Sorumu kullanarak colve ksh93benim sorumla olası çözümlere ilişkin ipuçlarını genişletmek için :

Tek bir denetim dışı karakterin genişliğini elde etmek için colfrom bsdmainutilson Debian'ı (diğer coluygulamalarla çalışmayabilir ) kullanarak:

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

Misal:

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

Bir dize için genişletildi:

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 kullanarak 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 kullanarak Text::CharWidth:

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.