Obtém a largura de exibição de uma string de caracteres

13

Qual seria o mais próximo de uma maneira portátil de obter a largura de exibição (em pelo menos um terminal (um que exibe caracteres na localidade atual com a largura correta)) de uma cadeia de caracteres de um script de shell.

Estou interessado principalmente na largura dos caracteres sem controle, mas soluções que levam em conta caracteres de controle como retrocesso, retorno de carro, tabulação horizontal também são bem-vindas.

Em outras palavras, estou procurando uma API shell em torno da função wcswidth() POSIX.

Esse comando deve retornar:

$ 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

Pode-se usar ksh93 de printf '%<n>Ls' que leva em consideração a largura de caracteres para preenchimento em <n> colunas, ou o comando col (com por exemplo printf '++%s\b\b--\n' <character> | col -b ) para tentar obter isso, há um módulo Text :: CharWidth perl , pelo menos, mas existem abordagens mais diretas ou portáveis.

Isso é mais ou menos uma continuação da outra pergunta que foi sobre exibir texto à direita de a tela para a qual você precisaria ter essa informação antes de exibir o texto.

    
por Stéphane Chazelas 23.11.2015 / 22:02

4 respostas

7

Em um emulador de terminal, pode-se usar o relatório de posição do cursor para obter posições antes / depois, por exemplo, de

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

e descubra a largura dos caracteres impressos no terminal. Como essa é uma seqüência de controle ECMA-48 (bem como VT100) suportada por quase todos os terminais que você provavelmente usará, ela é bastante portátil.

Para referência

Por fim, o emulador de terminal determina a largura imprimível devido a esses fatores:

  • as configurações do local afetam a maneira como uma string pode ser formatada, mas a série de bytes enviados ao terminal é interpretada com base em como o terminal é configurado (observando que algumas pessoas argumentarão que ele precisa ser UTF-8) a outra mão portabilidade foi o recurso solicitado na questão).
  • wcswidth sozinho não informa como os caracteres combinados são manipulados; O POSIX não menciona este aspecto na descrição dessa função.
  • alguns caracteres (desenho de linha, por exemplo), que pode ser considerado como largura única (em Unicode) "largura ambígua", solapando a portabilidade de um aplicativo usando wcswidth sozinho (veja por exemplo Capítulo 2. Configurando o Cygwin ). Por exemplo, xterm tem provisão para selecionar caracteres de largura dupla para configurações necessárias.
  • para manipular qualquer coisa que não sejam caracteres imprimíveis, você teria que confiar no emulador de terminal (a menos que você queira simular isso).

As APIs do Shell que chamam wcswidth são suportadas em graus variados:

São mais ou menos diretos: simulando wcswidth no caso de Perl, chamando o tempo de execução C de Ruby e Python. Você poderia até mesmo usar palavrões, por exemplo, do Python (que lidaria com caracteres combinados):

  • inicialize o terminal usando setupterm (nenhum texto é gravado na tela)
  • use a função filter (para linhas únicas)
  • desenhe o texto no início da linha com addstr , verificação de erro (no caso de ser muito longo) e, em seguida, para a posição final
  • se houver espaço, ajuste a posição inicial.
  • chame endwin (que não deve fazer a refresh )
  • escreva as informações resultantes sobre a posição inicial na saída padrão

O uso de palavrões para saída (ao invés de alimentar as informações de volta a um script ou chamar diretamente tput ) limparia toda a linha ( filter o limitaria a uma linha). p>     

por 23.11.2015 / 22:32
3

No meu .profile , eu chamo um script para determinar a largura de uma string em um terminal. Eu uso isso ao fazer logon no console de uma máquina em que não confio no conjunto de sistemas LC_CTYPE ou quando eu faço logon remotamente e não posso confiar em LC_CTYPE para corresponder ao lado remoto. Meu script consulta o terminal, em vez de chamar qualquer biblioteca, porque esse era o ponto principal no meu caso de uso: determinar a codificação do terminal.

Isso é frágil de várias maneiras:

  • modifica a exibição, por isso não é uma experiência muito boa para o usuário;
  • há uma condição de corrida se outro programa exibir algo na hora errada;
  • trava se o terminal não responder. (Há alguns anos eu perguntei como melhorar isso , mas Não foi um grande problema na prática, então eu nunca cheguei a mudar para essa solução.O único caso que encontrei de um terminal que não responde foi um Windows Emacs acessando arquivos remotos de uma máquina Linux com o plink method, e resolvi por usando o método plinkx em vez .

Isso pode ou não corresponder ao seu caso de 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='03110351';;
    *)
      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='3['
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

O script retorna a largura em seu status de retorno, recortado para 100. Uso da amostra:

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
    
por 24.11.2015 / 01:13
3

Para strings de uma linha, a implementação GNU de wc tem uma opção -L (a.k.a. --max-line-length ) que faz exatamente o que você está procurando (exceto os caracteres de controle).

    
por 29.01.2016 / 15:48
1

Para expandir as sugestões de possíveis soluções usando col e ksh93 na minha pergunta:

Usando o col de bsdmainutils no Debian (pode não funcionar com outras col implementações), para obter a largura de um único caractere sem controle:

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

Exemplo:

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

Prorrogado por uma 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"
}

Usando ksh93 ' 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 ' Text::CharWidth :

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' "$@"
}
    
por 28.01.2016 / 15:46