Ermittelt die Anzeigebreite einer Zeichenkette

15

Was wäre der beste Weg, um die Anzeigebreite (mindestens auf einem Terminal, auf dem Zeichen im aktuellen Gebietsschema mit der richtigen Breite angezeigt werden) einer Zeichenfolge aus einem Shell-Skript zu ermitteln?

Mich interessiert in erster Linie die Breite von Nicht-Steuerzeichen, aber auch Lösungen, die Steuerzeichen wie Rücktaste, Wagenrücklauf und horizontale Tabellierung berücksichtigen, sind willkommen.

Mit anderen Worten, ich suche nach einer Shell- API für die wcswidth()POSIX-Funktion.

Dieser Befehl sollte zurückgeben:

$ 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

Man könnte ksh93's verwenden printf '%<n>Ls', das die Zeichenbreite für das Auffüllen von <n>Spalten berücksichtigt , oder den colBefehl (zum Beispiel printf '++%s\b\b--\n' <character> | col -b), um dies abzuleiten. Es gibt perlmindestens ein Text :: CharWidth- Modul, aber es gibt direktere oder portablere Ansätze.

Das ist mehr oder weniger eine Fortsetzung der anderen Frage, bei der es um die Anzeige von Text auf der rechten Seite des Bildschirms ging, für die Sie diese Informationen benötigen würden, bevor Sie den Text anzeigen können.

Stéphane Chazelas
quelle

Antworten:

7

In einem Terminal-Emulator könnte man den Cursor-Positionsbericht verwenden, um Vorher / Nachher-Positionen abzurufen, z. B. von

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

und finden Sie heraus, wie breit die auf dem Terminal gedruckten Zeichen sind. Da dies eine ECMA-48-Steuersequenz (sowie eine VT100-Steuersequenz) ist, die von nahezu jedem Terminal unterstützt wird, das Sie wahrscheinlich verwenden, ist sie ziemlich portabel.

Als Referenz

    CSI Ps n-Gerätestatusbericht (DSR).
              ...
                Ps = 6 -> Cursorposition melden (CPR) [Zeile; Spalte].
              Ergebnis ist CSI r; c R

Letztendlich bestimmt der Terminalemulator die druckbare Breite aufgrund der folgenden Faktoren:

  • Die Einstellungen für das Gebietsschema wirken sich auf die Art und Weise aus, wie eine Zeichenfolge formatiert werden kann. Die an das Terminal gesendeten Bytefolgen werden jedoch je nach Konfiguration des Terminals interpretiert Portabilität war das in der Frage geforderte Merkmal).
  • wcswidthallein sagt nicht aus, wie das Kombinieren von Zeichen gehandhabt wird; POSIX erwähnt diesen Aspekt in der Beschreibung dieser Funktion nicht.
  • Einige Zeichen (z. B. Strichzeichnungen), die man als einfach voraussetzt, sind (in Unicode) "mehrdeutig", was die Portabilität einer Anwendung wcswidthallein untergräbt (siehe z. B. Kapitel 2. Einrichten von Cygwin ). xtermBeispielsweise ist die Auswahl von Zeichen mit doppelter Breite für Konfigurationen vorgesehen, die hierfür erforderlich sind.
  • Um etwas anderes als druckbare Zeichen zu verarbeiten, müssten Sie sich auf den Terminalemulator verlassen (es sei denn, Sie möchten dies simulieren).

Der Aufruf von Shell-APIs wcswidthwird in unterschiedlichem Maße unterstützt:

Diese sind mehr oder weniger direkt: wcswidthIm Fall von Perl simulieren , C-Laufzeit von Ruby und Python aufrufen. Sie könnten sogar Flüche verwenden, z. B. aus Python (die das Kombinieren von Zeichen handhaben würden):

  • initialisiere das Terminal mit setupterm (es wird kein Text auf den Bildschirm geschrieben)
  • benutze die filterFunktion (für einzelne Zeilen)
  • Zeichnen Sie den Text am Anfang der Zeile mit addstr, überprüfen Sie ihn auf Fehler (falls er zu lang ist) und dann auf die Endposition
  • Wenn noch Platz vorhanden ist, stellen Sie die Ausgangsposition ein.
  • anrufen endwin(was a nicht tun sollte refresh)
  • Schreiben Sie die resultierenden Informationen über die Startposition in die Standardausgabe

Die Verwendung von Flüchen für die Ausgabe (anstatt die Informationen an ein Skript zurückzugeben oder direkt aufzurufen tput) würde die gesamte Zeile löschen ( filterbeschränkt sie jedoch auf eine Zeile).

Thomas Dickey
quelle
Ich denke, das muss wirklich der einzige Weg sein. Wenn das Terminal keine Zeichen mit doppelter Breite unterstützt, spielt es keine Rolle, was wcswidth()zu irgendetwas zu sagen ist.
mikeserv
In der Praxis ist das einzige Problem, das ich mit dieser Methode hatte plink, das sich einstellt TERM=xterm, obwohl es auf keine Steuerungssequenz reagiert. Aber ich benutze keine sehr exotischen Terminals.
Gilles 'SO- hör auf böse zu sein'
Vielen Dank. Die Idee war jedoch, diese Informationen vor dem Anzeigen der Zeichenfolge auf dem Terminal abzurufen (um zu wissen, wo sie angezeigt werden sollen, ist dies eine Fortsetzung der kürzlich gestellten Frage zum Anzeigen einer Zeichenfolge rechts vom Terminal. Vielleicht hätte ich das erwähnen sollen Meine eigentliche Frage war jedoch, wie ich von der Shell zu wcswidth komme. @mikeserv, ja wcswidth () kann falsch sein, wie ein bestimmtes Terminal einen bestimmten String anzeigen würde, aber das ist so nah wie möglich an einer terminalunabhängigen Lösung und das ist, was col / ksh-printf auf meinem System verwendet.
Stéphane Chazelas
Ich bin mir dessen bewusst , dass, aber wcswidth nicht direkt zugänglich ist, außer über weniger portable Funktionen (man könnte dies in Perl tun, indem sie einige Annahmen - siehe search.cpan.org/dist/Text-CharWidth/CharWidth.pm ) . Die Frage nach der Rechtsausrichtung könnte übrigens (vielleicht) verbessert werden, indem die Zeichenfolge nach links unten geschrieben und dann mit den Steuerelementen für Cursorposition und Einfügung nach rechts unten verschoben wird.
Thomas Dickey
1
@ StéphaneChazelas - foldist anscheinend darauf ausgelegt, Mehrbyte- und Zeichen mit erweiterter Breite zu verarbeiten . So sollte es mit der Rücktaste umgehen: Die aktuelle Anzahl der Zeilenbreiten soll um eins verringert werden, obwohl die Anzahl niemals negativ werden soll. Das Dienstprogramm fold darf ein <newline> nicht unmittelbar vor oder nach einem <backspace> einfügen, es sei denn, das folgende Zeichen hat eine Breite von mehr als 1 und würde dazu führen, dass die Linienbreite die Breite überschreitet. vielleicht fold -w[num]und pr +[num]könnte irgendwie zusammengeschlossen werden?
mikeserv
5

Für einzeilige Strings verfügt die GNU-Implementierung von wcüber eine -L(aka --max-line-length) -Option, die genau das tut, wonach Sie suchen (mit Ausnahme der Steuerzeichen).

egmont
quelle
1
Vielen Dank. Ich hatte keine Ahnung, dass es die Anzeigebreite zurückgeben würde. Beachten Sie, dass die FreeBSD-Implementierung auch die Option -L enthält. Das Dokument gibt die Anzahl der Zeichen in der längsten Zeile zurück, aber mein Test scheint stattdessen die Anzahl der Bytes anzugeben (nicht die Anzeigebreite in jedem Fall). OS / X hat kein -L, obwohl ich erwartet hätte, dass es von FreeBSDs abstammt.
Stéphane Chazelas
Es scheint auch zu funktionieren tab(setzt Tabulatorstopps alle 8 Spalten voraus).
Stéphane Chazelas
Tatsächlich würde ich für mehr als einzeilige Zeichenfolgen sagen, dass sie genau das tun, wonach ich suche, da sie die LF-Steuerzeichen ordnungsgemäß handhaben .
Stéphane Chazelas
@ StéphaneChazelas: Hast du immer noch das Problem, dass dies die Anzahl der Bytes und nicht die Anzahl der Zeichen zurückgibt? Ich habe es an Ihren Daten getestet und erhalte die gewünschten Ergebnisse: wc -L <<< 'unix'→ 8,  wc -L <<< 'Stéphane'→ 8 und  wc -L <<< 'もで 諤奯ゞ'→ 11. PS Sie betrachten „Stéphane“ als neun Zeichen, von denen eines die Breite Null hat? Es sieht für mich aus wie acht Zeichen, von denen eines aus mehreren Bytes besteht.
G-Man sagt, Monica werde am
@ G-Man, ich bezog mich auf die FreeBSD-Implementierung, die in FreeBSD 12.0 und einem UTF-8-Gebietsschema immer noch Bytes zu zählen scheint. Beachten Sie, dass é mit einem U + 00E9-Zeichen oder einem U + 0065 (e) -Zeichen gefolgt von U + 0301 (Kombination von Akut) geschrieben werden kann. Letzteres ist das in der Frage gezeigte Zeichen.
Stéphane Chazelas
4

In my .profilerufe ich ein Skript auf, um die Breite eines Strings in einem Terminal zu bestimmen. Ich verwende dies, wenn ich mich an der Konsole eines Computers anmelde, auf dem ich der Systemgruppe nicht vertraue LC_CTYPE, oder wenn ich mich remote anmelde und nicht vertraue LC_CTYPE, dass es mit der Remote-Seite übereinstimmt. Mein Skript fragt das Terminal ab, anstatt eine Bibliothek aufzurufen, denn das war der springende Punkt in meinem Anwendungsfall: Ermitteln Sie die Codierung des Terminals.

Das ist in mehrfacher Hinsicht fragil:

  • es verändert die Anzeige, so dass es keine sehr schöne Benutzererfahrung ist;
  • Es liegt eine Rennbedingung vor, wenn ein anderes Programm etwas zur falschen Zeit anzeigt.
  • Es stürzt ab, wenn das Terminal nicht antwortet. (Vor ein paar Jahren habe ich gefragt, wie ich das verbessern könnte , aber in der Praxis war das kein großes Problem. Daher bin ich nie auf diese Lösung umgestiegen. Der einzige Fall, bei dem ich auf ein Terminal gestoßen bin, das nicht reagiert, war Ein Windows Emacs greift mit der plinkMethode von einem Linux-Computer auf Remote-Dateien zu , und ich habe es mithilfe der plinkxMethode gelöst .)

Dies kann Ihrem Anwendungsfall entsprechen oder nicht.

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

Das Skript gibt die Breite in ihrem Rückgabestatus zurück, der auf 100 begrenzt ist.

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
Gilles 'SO - hör auf böse zu sein'
quelle
Dies war hilfreich für mich (obwohl ich meistens Ihre gekürzte Version verwendet habe ). Ich habe die Verwendung durch Hinzufügen printf "\r%*s\r" $((${#text}+8)) " ";am Ende von etwas schöner gemacht cleanup(Hinzufügen von 8 ist willkürlich; es muss lang genug sein, um die breitere Ausgabe älterer Gebietsschemata abzudecken, aber schmal genug, um einen Zeilenumbruch zu vermeiden). Dies macht den Test unsichtbar, setzt jedoch voraus, dass in der Zeile nichts gedruckt wurde (was in a in Ordnung ist ~/.profile)
Adam Katz
Tatsächlich scheint es , von einem kleinen Experiment , dass in zsh (5.7.1) , die Sie gerade tun können , text="Éé"und dann ${#text}werden Ihnen die Anzeigebreite (ich 4in einem Nicht-Unicode - Terminal und 2in einem Unicode-kompatibelen Terminal). Dies gilt nicht für Bash.
Adam Katz
@AdamKatz ${#text}gibt Ihnen nicht die Anzeigebreite. Hier sehen Sie die Anzahl der Zeichen in der vom aktuellen Gebietsschema verwendeten Codierung. Was für meinen Zweck unbrauchbar ist, da ich die Codierung des Terminals bestimmen möchte. Dies ist nützlich, wenn Sie die Anzeigebreite aus einem anderen Grund benötigen, sie jedoch nicht genau ist, da nicht jedes Zeichen eine Einheit breit ist. Zum Beispiel haben kombinierte Akzente eine Breite von 0 und chinesische Ideogramme eine Breite von 2.
Gilles 'SO - hör auf, böse zu sein'
Ja, guter Punkt. Es mag Stéphanes Frage befriedigen, aber nicht Ihre ursprüngliche Absicht (was ich eigentlich auch tun wollte, um Ihren Code anzupassen). Hoffentlich war mein erster Kommentar hilfreich für Sie, Gilles.
Adam Katz
3

Eric Pruitt hat eine beeindruckende Implementierung von wcwidth()und wcswidth()in Awk geschrieben, die unter wcwidth.awk verfügbar ist . Es bietet hauptsächlich 4 Funktionen

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

wo wcscolumns()auch nicht druckbare Zeichen toleriert werden.

$ 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

Ich habe ein Problem mit der Behandlung von Tabulatoren geöffnet, da wcscolumns($'My sign is\t鼠鼠')diese größer als 14 sein sollten. Update: Eric hat die Funktion wcsexpand()zum Erweitern von Tabulatoren zu Leerzeichen hinzugefügt :

$ 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
Xebeche
quelle
1

Um die Hinweise auf mögliche Lösungen mit colund ksh93in meiner Frage zu erweitern:

Verwenden von colfrom bsdmainutilson Debian (funktioniert möglicherweise nicht mit anderen colImplementierungen), um die Breite eines einzelnen Nicht-Steuerzeichens zu erhalten:

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

Beispiel:

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

Erweitert um einen String:

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

Verwenden von 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}))"
}

Verwenden von perl's Text::CharWidth:

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
Stéphane Chazelas
quelle