Ottieni la larghezza di visualizzazione di una stringa di caratteri


15

Quale sarebbe il modo più vicino a un modo portatile per ottenere la larghezza di visualizzazione (almeno su un terminale (uno che visualizza i caratteri nella locale corrente con la larghezza corretta)) di una stringa di caratteri da uno script di shell.

Sono principalmente interessato alla larghezza dei caratteri non di controllo, ma sono benvenute soluzioni che tengono conto dei caratteri di controllo come backspace, ritorno a capo, tabulazione orizzontale.

In altre parole, sto cercando un'API shell attorno alla wcswidth()funzione POSIX.

Tale comando dovrebbe restituire:

$ 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

Si potrebbe usare ksh93's printf '%<n>Ls'che tenga conto della larghezza di carattere per imbottitura per <n>le colonne, o il colcomando (con per esempio printf '++%s\b\b--\n' <character> | col -b) per cercare di ricavare che, c'è un testo :: CharWidth perlmodulo almeno, ma ci sono approcci più diretti o portatili.

Questo è più o meno un seguito su quell'altra domanda che riguardava la visualizzazione di testo a destra dello schermo per il quale sarebbe necessario disporre di tali informazioni prima di visualizzare il testo.


Risposte:


7

In un emulatore di terminale, è possibile utilizzare il rapporto sulla posizione del cursore per ottenere posizioni prima / dopo, ad es. Da

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

e scopri quanto sono larghi i caratteri stampati sul terminale. Dal momento che si tratta di una sequenza di controllo ECMA-48 (così come VT100) supportata da quasi tutti i terminali che è probabile che si utilizzi, è abbastanza portatile.

Per riferimento

    CSI Ps n Device Status Report (DSR).
              ...
                Ps = 6 -> Segnala posizione cursore (CPR) [riga; colonna].
              Il risultato è CSI r; c R

In definitiva, l'emulatore di terminale determina la larghezza stampabile, a causa di questi fattori:

  • le impostazioni locali influenzano il modo in cui una stringa può essere formattata, ma le serie di byte inviate al terminale vengono interpretate in base alla configurazione del terminale (notando che alcune persone sostengono che deve essere UTF-8, mentre d'altra parte la portabilità era la caratteristica richiesta nella domanda).
  • wcswidthda solo non dice come vengono gestiti i personaggi combinati; POSIX non menziona questo aspetto nella descrizione di quella funzione.
  • alcuni caratteri (ad esempio il disegno a tratteggio) che si potrebbe dare per scontato come larghezza singola sono (in Unicode) "larghezza ambigua", minando la portabilità di un'applicazione usando wcswidthda sola (vedere ad esempio il Capitolo 2. Impostazione di Cygwin ). xtermad esempio, prevede la selezione di caratteri a doppia larghezza per le configurazioni necessarie.
  • per gestire qualsiasi cosa diversa dai caratteri stampabili, dovresti fare affidamento sull'emulatore di terminale (a meno che tu non voglia simularlo).

Le chiamate API Shell wcswidthsono supportate a vari livelli:

Sono più o meno diretti: simulano wcswidthnel caso di Perl, chiamando C runtime da Ruby e Python. Puoi persino usare maledizioni, ad esempio, da Python (che gestirà la combinazione di personaggi):

  • inizializza il terminale usando setupterm (non viene scritto alcun testo sullo schermo)
  • usa la filterfunzione (per linee singole)
  • disegna il testo all'inizio della riga con addstr, verificando la presenza di errori (nel caso in cui sia troppo lungo), quindi la posizione finale
  • se c'è spazio, regolare la posizione di partenza.
  • call endwin(che non dovrebbe fare a refresh)
  • scrivere le informazioni risultanti sulla posizione iniziale nell'output standard

L'uso delle maledizioni per l' output (anziché restituire le informazioni a uno script o chiamare direttamente tput) cancellerebbe l'intera linea ( filterlimitandola a una linea).


penso che questo debba essere l'unico modo, davvero. se il terminale non supporta i caratteri a doppia larghezza, allora non ha molta importanza ciò che wcswidth()c'è da dire su qualcosa.
mikeserv,

In pratica, l'unico problema che ho avuto con questo metodo è plinkche si pone TERM=xtermanche se non risponde a nessuna sequenza di controllo. Ma non uso terminali molto esotici.
Gilles 'SO- smetti di essere malvagio' il

Grazie. ma l'idea era quella di ottenere tali informazioni prima di visualizzare la stringa sul terminale (per sapere dove visualizzarla, si tratta di un follow-up sulla recente domanda sulla visualizzazione di una stringa sulla destra del terminale, forse avrei dovuto menzionare che sebbene la mia vera domanda fosse davvero su come arrivare a wcswidth dalla shell). @mikeserv, yes wcswidth () potrebbe sbagliare su come un terminale specifico visualizzerebbe una stringa particolare, ma è il più vicino possibile a una soluzione indipendente dal terminale ed è quello che col / ksh-printf usa sul mio sistema.
Stéphane Chazelas,

Ne sono consapevole, ma wcswidth non è direttamente accessibile se non tramite funzionalità meno portatili (potresti farlo in perl, facendo alcune ipotesi - vedi search.cpan.org/dist/Text-CharWidth/CharWidth.pm ) . La domanda di allineamento a destra potrebbe essere (forse) migliorata scrivendo la stringa in basso a sinistra e quindi usando la posizione del cursore e i controlli di inserimento per spostarla in basso a destra.
Thomas Dickey,

1
@ StéphaneChazelas - foldè apparentemente progettato per gestire caratteri multi-byte e di larghezza estesa . Ecco come dovrebbe gestire il backspace: l'attuale conteggio della larghezza della linea deve essere ridotto di uno, sebbene il conteggio non diventi mai negativo. L'utilità di piegatura non deve inserire una <nuova> immediatamente prima o dopo qualsiasi <subo inverso>, a meno che il carattere seguente abbia una larghezza maggiore di 1 e farebbe sì che la larghezza della linea superi la larghezza. forse fold -w[num]e pr +[num]potrebbe essere unito in qualche modo?
mikeserv,

5

Per le stringhe di una riga, l'implementazione GNU di wcha un'opzione -L(aka --max-line-length) che fa esattamente quello che stai cercando (tranne i caratteri di controllo).


1
Grazie. Non avevo idea che avrebbe restituito la larghezza del display. Nota che l'implementazione di FreeBSD ha anche un'opzione -L, il documento dice che restituisce il numero di caratteri nella riga più lunga, ma il mio test sembra indicare invece un numero di byte (non la larghezza di visualizzazione in nessun caso). OS / X non ha -L, anche se mi sarei aspettato che derivasse da FreeBSD.
Stéphane Chazelas,

Sembra gestire tabanche (presuppone che le tabulazioni si fermino ogni 8 colonne).
Stéphane Chazelas,

In realtà, per le stringhe su più di una riga, direi che fa esattamente quello che sto cercando, in quanto gestisce correttamente i caratteri di controllo LF .
Stéphane Chazelas,

@ StéphaneChazelas: Stai ancora riscontrando il problema che questo restituisce il numero di byte anziché il numero di caratteri? L'ho testato sui tuoi dati e ho ottenuto i risultati desiderati: wc -L <<< 'unix'→ 8,  wc -L <<< 'Stéphane'→ 8 e  wc -L <<< 'もで 諤奯ゞ'→ 11. PS Considera "Stéphane" composto da nove caratteri, uno dei quali ha larghezza zero? Mi sembrano otto caratteri, uno dei quali è multi-byte.
G-Man dice "Ripristina Monica" il

@G-Man, mi riferivo all'implementazione di FreeBSD, che in FreeBSD 12.0 e una locale UTF-8 sembra ancora contare i byte. Nota che é può essere scritto usando un carattere U + 00E9 o un carattere U + 0065 (e) seguito da U + 0301 (combinando accento acuto), quest'ultimo essendo quello mostrato nella domanda.
Stéphane Chazelas,

4

Nel mio .profile, chiamo uno script per determinare la larghezza di una stringa su un terminale. Lo uso quando eseguo l'accesso sulla console di una macchina in cui non mi fido del set di sistemi LC_CTYPEo quando accedo in remoto e non posso fidarmi LC_CTYPEdi abbinare il lato remoto. Il mio script interroga il terminale, piuttosto che chiamare qualsiasi libreria, perché quello era il punto nel mio caso d'uso: determinare la codifica del terminale.

Questo è fragile in diversi modi:

  • modifica il display, quindi non è un'esperienza utente molto piacevole;
  • c'è una condizione di gara se un altro programma mostra qualcosa nel momento sbagliato;
  • si blocca se il terminale non risponde. (Qualche anno fa ho chiesto come migliorare , ma in pratica non è stato un grosso problema, quindi non sono mai riuscito a passare a quella soluzione. L'unico caso che ho riscontrato di un terminale che non rispondeva era un Windows Emacs che accede ai file remoti da una macchina Linux con il plinkmetodo e l'ho risolto usando plinkxinvece il metodo .)

Questo potrebbe corrispondere o meno al tuo caso d'uso.

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

Lo script restituisce la larghezza nel suo stato di ritorno, tagliato a 100. Esempio di utilizzo:

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

Questo mi è stato utile (anche se ho usato principalmente la tua versione ridotta ). Ho reso il suo utilizzo un po 'più bello aggiungendo printf "\r%*s\r" $((${#text}+8)) " ";alla fine di cleanup(aggiungere 8 è arbitrario; deve essere abbastanza lungo da coprire l'output più ampio di locali più vecchi ma abbastanza stretto da evitare un avvolgimento di riga). Questo rende invisibile il test, anche se presuppone che non sia stato stampato nulla sulla riga (che va bene in a ~/.profile)
Adam Katz,

In realtà, sembra da una piccola sperimentazione che in zsh (5.7.1) si può semplicemente fare text="Éé"e quindi ${#text}vi darà la larghezza di visualizzazione (ottengo 4in un terminale non unicode e 2in un terminale conforme unicode). Questo non è vero per bash.
Adam Katz,

@AdamKatz ${#text}non ti dà la larghezza del display. Ti dà il numero di caratteri nella codifica utilizzata dalla locale corrente. Che è inutile per il mio scopo poiché voglio determinare la codifica del terminale. È utile se si desidera la larghezza del display per qualche altro motivo, ma non è preciso perché non tutti i caratteri sono larghi di un'unità. Ad esempio, la combinazione di accenti ha una larghezza di 0 e gli ideogrammi cinesi hanno una larghezza di 2.
SO di Gilles

Sì, buon punto. Potrebbe soddisfare la domanda di Stéphane ma non il tuo intento originale (che in realtà è quello che volevo fare anch'io, quindi adattando il tuo codice). Spero che il mio primo commento ti sia stato utile, Gilles.
Adam Katz,

3

Eric Pruitt ha scritto un'impressionante implementazione di wcwidth()e wcswidth()in Awk disponibile su wcwidth.awk . Fornisce principalmente 4 funzioni

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

dove wcscolumns()tollera anche caratteri non stampabili.

$ 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

Ho aperto un problema che chiedeva la gestione dei TAB poiché wcscolumns($'My sign is\t鼠鼠')dovrebbe essere maggiore di 14. Aggiornamento: Eric ha aggiunto la funzione wcsexpand()per espandere i TAB negli spazi:

$ 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

Per espandere i suggerimenti su possibili soluzioni utilizzando cole ksh93nella mia domanda:

Usare il colda bsdmainutilssu Debian (potrebbe non funzionare con altre colimplementazioni), per ottenere la larghezza di un singolo carattere non di controllo:

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

Esempio:

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

Esteso per una stringa:

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

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

Usando perl's Text::CharWidth:

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.