Como contar POSIX-ly o número de linhas em uma variável de string?

10

Eu sei que posso fazer isso no Bash:

wc -l <<< "${string_variable}"

Basicamente, tudo o que encontrei envolvia o <<<operador Bash.

Mas no shell POSIX, <<<é indefinido, e não consegui encontrar uma abordagem alternativa por horas. Tenho certeza de que existe uma solução simples para isso, mas, infelizmente, não encontrei até agora.

LinuxSecurityFreak
fonte

Respostas:

11

A resposta simples é que wc -l <<< "${string_variable}"é um atalho do ksh / bash / zsh para printf "%s\n" "${string_variable}" | wc -l.

Na verdade, existem diferenças na maneira <<<e um trabalho de canal: <<<cria um arquivo temporário que é passado como entrada para o comando, enquanto |cria um canal. No bash e no pdksh / mksh (mas não no ksh93 ou zsh), o comando no lado direito do pipe é executado em uma subshell. Mas essas diferenças não importam neste caso particular.

Observe que, em termos de linhas de contagem, isso pressupõe que a variável não está vazia e não termina com uma nova linha. Não terminando com uma nova linha é o caso em que a variável é o resultado de uma substituição de comando, portanto, na maioria dos casos, você obtém o resultado certo, mas obtém 1 para a sequência vazia.

Existem duas diferenças entre var=$(somecommand); wc -l <<<"$var"e somecommand | wc -l: usar uma substituição de comando e uma variável temporária retira as linhas em branco no final, esquece se a última linha de saída terminou em uma nova linha ou não (sempre ocorre se o comando gerar um arquivo de texto não vazio válido) e superconta em um se a saída estiver vazia. Se você deseja preservar o resultado e contar as linhas, pode fazê-lo anexando algum texto conhecido e retirando-o no final:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"
Gilles 'SO- parar de ser mau'
fonte
1
O @Inian Keeping wc -lé exatamente equivalente ao original: <<<$fooadiciona uma nova linha ao valor de $foo(mesmo que $fooestivesse vazio). Explico na minha resposta por que isso pode não ter sido o que se queria, mas foi o que foi perguntado.
Gilles 'SO- stop be evil'
2

Não estando em conformidade com os shell internos, usando utilitários externos como grepe awkcom opções compatíveis com POSIX,

string_variable="one
two
three
four"

Fazendo greppara corresponder ao início das linhas

printf '%s' "${string_variable}" | grep -c '^'
4

E com awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

Observe que algumas das ferramentas GNU, especialmente, GNU grepnão respeitam a POSIXLY_CORRECT=1opção de executar a versão POSIX da ferramenta. No grepúnico comportamento afetado pela configuração, a variável será a diferença no processamento da ordem dos sinalizadores da linha de comando. A partir da documentação ( grepmanual GNU ), parece que

POSIXLY_CORRECT

Se definido, o grep se comporta conforme o POSIX exige; caso contrário, grepse comportará mais como outros programas GNU. O POSIX requer que as opções que seguem os nomes dos arquivos sejam tratadas como nomes de arquivo; por padrão, essas opções são permutadas para a frente da lista de operandos e são tratadas como opções.

Consulte Como usar POSIXLY_CORRECT no grep?

Inian
fonte
2
Certamente wc -lainda é viável aqui?
Michael Homer
@MichaelHomer: Pelo que observei, wc -lprecisa de um fluxo delimitado por uma nova linha adequada (com um \ \ n final no final para contar corretamente). Não se pode usar um simples FIFO para usar com printf, por exemplo, printf '%s' "${string_variable}" | wc -lpode não funcionar como esperado, mas <<<seria por causa da fuga \nanexado pela herestring
Inian
1
Isso era o que printf '%s\n'estava fazendo, antes que você tirou ...
Michael Homer
1

A string here <<<é praticamente uma versão de uma linha do documento here <<. O primeiro não é um recurso padrão, mas o último é. Você também pode usar <<neste caso. Estes devem ser equivalentes:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

No entanto, observe que ambos adicionam uma nova linha extra no final de $somevar, por exemplo, isso imprime 6, mesmo que a variável tenha apenas cinco linhas:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

Com printf, você pode decidir se deseja a nova linha adicional ou não:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Mas observe que wcconta apenas linhas completas (ou o número de caracteres de nova linha na sequência). grep -c ^também deve contar o fragmento de linha final.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(É claro que você também pode contar as linhas inteiramente no shell usando a ${var%...}expansão para removê-las uma por vez em um loop ...)

ilkkachu
fonte
0

Nos casos surpreendentemente frequentes em que o que você realmente precisa fazer é processar todas as linhas não vazias dentro de uma variável de alguma forma (incluindo contá-las), você pode definir o IFS como apenas uma nova linha e usar o mecanismo de divisão de palavras do shell para quebrar as linhas não vazias separadas.

Por exemplo, aqui está uma pequena função shell que totaliza as linhas não vazias dentro de todos os argumentos fornecidos:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

Parênteses, em vez de chaves, são usados ​​aqui para formar o comando composto para o corpo da função. Isso faz com que a função seja executada em um subshell para que não polua a variável IFS do mundo externo e a configuração de expansão do nome do caminho em todas as chamadas.

Se você deseja iterar sobre linhas não vazias, faça o mesmo:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

Manipular o IFS dessa maneira é uma técnica frequentemente ignorada, também útil para fazer coisas como analisar nomes de caminho que podem conter espaços de entrada colunar delimitada por tabulação. No entanto, você precisa estar ciente de que a remoção deliberada do caractere de espaço geralmente incluído na configuração padrão do IFS de space-tab-newline pode acabar desabilitando a divisão de palavras em locais onde você normalmente esperaria vê-lo.

Por exemplo, se você estiver usando variáveis ​​para criar uma linha de comando complicada para algo como ffmpeg, convém incluir -vf scale=$scaleapenas quando a variável scaleestiver definida como algo não vazio. Normalmente, você pode conseguir isso, ${scale:+-vf scale=$scale}mas se o IFS não incluir seu caractere de espaço usual no momento em que essa expansão de parâmetro for concluída, o espaço entre -vfe scale=não será usado como um separador de palavras e ffmpegserá passado -vf scale=$scalecomo um único argumento, que não vai entender.

Para corrigir isso, você quer necessidade de certificar-se de IFS foi criado mais normalmente antes de fazer a ${scale}expansão, ou fazer duas expansões: ${scale:+-vf} ${scale:+scale=$scale}. A palavra divisão que o shell faz no processo de análise inicial das linhas de comando, em oposição à divisão durante a fase de expansão do processamento dessas linhas de comando, não depende do IFS.

Outra coisa que poderia valer a pena, se você fizer esse tipo de coisa, seria criar duas variáveis ​​globais do shell para conter apenas uma guia e apenas uma nova linha:

t=' '
n='
'

Dessa forma, você pode apenas incluir $te $nem expansões nas quais você precisa de guias e novas linhas, em vez de colocar todo o seu código no espaço em branco entre aspas. Se você preferir evitar o espaço em branco citado em um shell POSIX que não tem outro mecanismo para fazê-lo, printfpode ajudar, embora você precise de um pouco de mexer para contornar a remoção de novas linhas à direita nas expansões de comando:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

Às vezes, definir o IFS como se fosse uma variável de ambiente por comando funciona bem. Por exemplo, aqui está um loop que lê um nome de caminho que pode conter espaços e um fator de escala de cada linha de um arquivo de entrada delimitado por tabulação:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

Nesse caso, o readbuilt-in vê o IFS definido como apenas uma guia, para não dividir a linha de entrada que lê nos espaços também. Mas IFS=$t set -- $lines não funciona: o shell se expande à $linesmedida que constrói os setargumentos do builtin antes de executar o comando, portanto, a configuração temporária do IFS de uma maneira que se aplica somente durante a execução do próprio buildin chega tarde demais. É por isso que os trechos de código que eu dei acima de tudo definem o IFS em uma etapa separada e por que eles precisam lidar com a questão de preservá-lo.

flabdablet
fonte