Número de caracteres na saída de um comando shell

12

Estou escrevendo um script que precisa calcular o número de caracteres na saída de um comando em uma única etapa .

Por exemplo, o uso do comando readlink -f /etc/fstabdeve retornar 10porque a saída desse comando tem 10 caracteres.

Isso já é possível com variáveis ​​armazenadas usando o seguinte código:

variable="somestring";
echo ${#variable};
# 10

Infelizmente, o uso da mesma fórmula com uma sequência gerada por comando não funciona:

${#(readlink -f /etc/fstab)};
# bash: ${#(readlink -f /etc/fstab)}: bad substitution

Entendo que é possível fazer isso primeiro salvando a saída em uma variável:

variable=$(readlink -f /etc/fstab);
echo ${#variable};

Mas eu gostaria de remover a etapa extra.

Isso é possível? É preferível a compatibilidade com o shell Almquist (sh) usando apenas utilitários embutidos ou padrão.

user339676
fonte
1
A saída de readlink -f /etc/fstabé de 11 caracteres. Não esqueça a nova linha. Caso contrário, você verá /etc/fstabluser@cern:~$ quando o executou a partir de um shell.
Phil Geada
@PhilFrost você parece ter um prompt engraçado, você trabalha no CERN?
Dmitry Grigoryev 24/02

Respostas:

9

Com o GNU expr :

$ expr length + "$(readlink -f /etc/fstab)"
10

O +há uma característica especial do GNU exprpara garantir que o próximo argumento é tratado como uma string, mesmo se ele passa a ser um exproperador como match, length, +...

O exemplo acima eliminará qualquer nova linha de saída posterior. Para contornar isso:

$ expr length + "$(readlink -f /etc/fstab; printf .)" - 2
10

O resultado foi subtraído para 2 porque a nova linha final de readlinke o personagem .que adicionamos.

Com a string Unicode, exprparece não funcionar, pois retorna o comprimento da string em bytes, em vez da contagem de caracteres (consulte a linha 654 )

$ LC_ALL=C.UTF-8 expr length ăaa
4

Então, você pode usar:

$ printf "ăaa" | LC_ALL=C.UTF-8 wc -m
3

POSIXAMENTE:

$ expr " $(readlink -f /etc/fstab; printf .)" : ".*" - 3
10

O espaço antes da substituição do comando evita que o comando seja travado com o início da string -, portanto, precisamos subtrair 3.

cuonglm
fonte
Obrigado! Parece que seu terceiro exemplo funciona mesmo sem o LC_ALL=C.UTF-8, o que simplifica significativamente as coisas se a codificação da string não for conhecida antecipadamente.
user339676
2
expr length $(echo "*")- Não. Pelo menos usar aspas duplas: expr length "$(…)". Mas isso retira as novas linhas do comando, é um recurso inevitável da substituição de comandos. (Você pode contornar isso, mas, em seguida, a resposta torna-se ainda mais complexa.)
Gilles 'SO parada sendo mal'
6

Não tenho certeza de como fazer isso com os shell builtins (o Gnouc é embora ), mas as ferramentas padrão podem ajudar:

  1. Você pode usar o wc -mque conta caracteres. Infelizmente, ele também conta a nova linha final, então você precisa se livrar disso primeiro:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. Você pode, é claro, usar awk

    readlink -f /etc/fstab | awk '{print length($0)}'
  3. Ou Perl

    readlink -f /etc/fstab | perl -lne 'print length'
terdon
fonte
Você quer dizer que expré um built-in? Em qual concha?
mikeserv
5

Eu costumo fazer assim:

$ echo -n "$variable" | wc -m
10

Para executar comandos, eu o adaptaria assim:

$ echo -n "$(readlink -f /etc/fstab)" | wc -m
10

Essa abordagem é semelhante ao que você estava fazendo em suas duas etapas, exceto que as estamos combinando em uma única linha.

slm
fonte
2
Você deve usar em -mvez de -c. Com caracteres unicode, sua abordagem será interrompida.
cuonglm
1
Por que não simplesmente readlink -f /etc/fstab | wc -m?
Phil Frost
1
Por que você usa esse método não confiável em vez de ${#variable}? Pelo menos use aspas duplas echo -n "$variable", mas isso ainda falha se, por exemplo, o valor de variablefor -e. Ao usá-lo em combinação com uma substituição de comando, lembre-se de que as novas linhas finais são removidas.
Gilles 'SO- stop be evil'
@philfrost b / c o que eu mostrei construído com base no que a op já estava pensando. Também funciona para qualquer cmds que ele possa ter configurado anteriormente no vars e queira seus comprimentos posteriores. Terdon também já tem esse exemplo.
slm
1

Você pode ligar para utilitários externos (veja outras respostas), mas eles tornarão seu script mais lento e é difícil acertar o encanamento.

Zsh

No zsh, você pode escrever ${#$(readlink -f /etc/fstab)}para obter o tamanho da substituição do comando. Observe que esse não é o tamanho da saída do comando, é o comprimento da saída sem nenhuma nova linha à direita.

Se você deseja o comprimento exato da saída, produza um caractere extra não-nova linha no final e subtrai um.

$((${#$(readlink -f /etc/fstab; echo .)} - 1))

Se o que você deseja é a carga útil na saída do comando, é necessário subtrair duas aqui, porque a saída de readlink -fé o caminho canônico mais uma nova linha.

$((${#$(readlink -f /etc/fstab; echo .)} - 2))

Isso difere do ${#$(readlink -f /etc/fstab)}caso raro, mas possível, em que o próprio caminho canônico termina em uma nova linha.

Para este exemplo específico, você não precisa de nenhum utilitário externo, porque o zsh possui uma construção interna equivalente a ela readlink -f, através do modificador de histórico A.

echo /etc/fstab(:A)

Para obter o comprimento, use o modificador de histórico em uma expansão de parâmetro:

${#${:-/etc/fstab}:A}

Se você tiver o nome do arquivo em uma variável filename, seria ${#filename:A}.

Conchas tipo Bourne / POSIX

Nenhum dos shells Bourne / POSIX puros (Bourne, ash, mksh, ksh93, bash, yash ...) tem qualquer extensão semelhante que eu conheça. Se você precisar aplicar uma substituição de parâmetro na saída de uma substituição de comando ou aninhar substituições de parâmetro, use estágios sucessivos.

Você pode inserir o processamento em uma função, se desejar.

command_output_length_sans_trailing_newlines () {
  set -- "$("$@")"
  echo "${#1}"
}

ou

command_output_length () {
  set -- "$("$@"; echo .)"
  echo "$((${#1} - 1))"
}

mas geralmente não há benefício; exceto com ksh93, isso faz com que um fork adicional seja capaz de usar a saída da função, tornando o script mais lento e raramente há benefícios de legibilidade.

Mais uma vez, a saída de readlink -fé o caminho canônico mais uma nova linha; se você quiser o comprimento do caminho canônico, subtraia 2 em vez de 1 pol command_output_length. O uso command_output_length_sans_trailing_newlinesfornece o resultado certo apenas quando o caminho canônico em si não termina em uma nova linha.

Bytes vs caracteres

${#…}deve ter o comprimento em caracteres, não em bytes, o que faz a diferença nos códigos de idioma multibyte. Versões razoavelmente atualizadas do ksh93, bash e zsh calculam o comprimento em caracteres de acordo com o valor LC_CTYPEno momento em que a ${#…}construção é expandida. Muitos outros shells comuns realmente não suportam localizações multibyte: a partir do traço 0.5.7, mksh 46 e posh 0.12.3, ${#…}retorna o comprimento em bytes. Se você deseja que o tamanho dos caracteres seja confiável, use o wcutilitário:

$(readlink -f /etc/fstab | wc -m)

Desde que $LC_CTYPEdesigne um código de idioma válido, você pode ter certeza de que isso ocorrerá erro (em uma plataforma antiga ou restrita que não suporta códigos de idioma com vários bytes) ou retornará o tamanho correto em caracteres. (Para Unicode, "comprimento em caracteres" significa o número de pontos de código - o número de glifos é mais uma história, devido a complicações como a combinação de caracteres.)

Se você deseja o comprimento em bytes, defina LC_CTYPE=Ctemporariamente ou use em wc -cvez de wc -m.

A contagem de bytes ou caracteres com wcinclui qualquer nova linha à direita do comando. Se você deseja o comprimento do caminho canônico em bytes, é

$(($(readlink -f /etc/fstab | wc -c) - 1))

Para obtê-lo em caracteres, subtraia 2.

Gilles 'SO- parar de ser mau'
fonte
@cuonglm Não, você precisa subtrair 1. echo .adiciona dois caracteres, mas o segundo caractere é uma nova linha à direita que é removida pela substituição do comando.
Gilles 'SO- stop be evil'
A nova linha é de readlinksaída, mais o .by echo. Nós dois concordamos em echo .adicionar dois caracteres, mas a nova linha à direita foi removida. Tente com printf .ou veja minha resposta unix.stackexchange.com/a/160499/38906 .
amigos estão dizendo sobre cuonglm
@cuonglm A pergunta perguntou o número de caracteres na saída do comando. A saída de readlinké o destino do link mais uma nova linha.
Gilles 'SO- stop be evil'
0

Isso funciona, dashmas exige que o var direcionado esteja definitivamente vazio ou não definido. É por isso que na verdade são dois comandos - eu esvazio explicitamente $lno primeiro:

l=;printf '%.slen is %d and result is %s\n' \
    "${l:=$(readlink -f /etc/fstab)}" "${#l}" "$l"

RESULTADO

len is 10 and result is /etc/fstab

Isso é tudo embutido no shell - não incluindo o readlinkclaro -, mas avaliá-lo no shell atual dessa maneira implica que você deve fazer a atribuição antes de obter o len, e é por isso que eu %.silencio o primeiro argumento na printfstring de formato e o adiciono novamente para o valor literal no printffinal da lista de argumentos.

Com eval:

l=$(readlink -f /etc/fstab) eval 'l=${#l}:$l'
printf %s\\n "$l"

RESULTADO

10:/etc/fstab

Você pode se aproximar da mesma coisa, mas em vez da saída em uma variável no primeiro comando, você a obtém no stdout:

PS4='${#0}:$0' dash -cx '2>&1' "$(readlink -f /etc/fstab)"

... que escreve ...

10:/etc/fstab

... para arquivar o descritor 1 sem atribuir nenhum valor a nenhum vars no shell atual.

mikeserv
fonte
1
Não era exatamente isso que o OP queria evitar? "Entendo que é possível fazer isso primeiro salvando a saída em uma variável: variable=$(readlink -f /etc/fstab); echo ${#variable};mas gostaria de remover a etapa extra".
terdon
@terdon, provavelmente eu não entendi, mas tive a impressão de que o ponto e vírgula era o problema e não a variável. É por isso que eles obtêm o len e o output em um único comando simples, usando apenas shell builtins. O shell não executa o readlink, em seguidaexpr , o exec , por exemplo. Provavelmente importa se, de alguma maneira, obter o lençol ocluir o valor, que eu admito que estou tendo dificuldade em entender por que isso pode acontecer, mas suspeito que possa haver um caso em que isso tenha importância.
mikeserv
1
A evalpropósito, a propósito, é provavelmente o mais limpo aqui - ele atribui a saída e o len ao mesmo nome de var em uma única execução - muito perto de ser feito l=length(l):out(l). Fazendo expr length $(command) faz ocluir o valor em favor do len, pela maneira.
mikeserv