
Quiero contar el número de líneas en una tubería y luego continuar con la tubería dependiendo del resultado.
Lo intenté
x=$(printf 'faa\nbor\nbaz\n' \
| tee /dev/stderr | wc -l) 2>&1 \
| if [[ $x -ge 2 ]]; then
grep a
else
grep b
fi
Pero no filtra nada (ni para "a" ni para "b"). Eso fue bastante inesperado ya que al menos estos funcionan como se esperaba:
printf 'faa\nbor\nbaz\n' | if true; then grep a; else grep b; fi
printf 'faa\nbor\nbaz\n' | if false; then grep a; else grep b; fi
Parece que no puedo redirigir el stderr desde dentro de la sustitución del comando ya que esto tampoco funciona (en bash). Imprime las tres líneas:
x=$(printf 'faa\nbor\nbaz\n' | tee /dev/stderr | wc -l) 2>&1 | grep a
En zsh solo imprime dos líneas.
Pero en ambos shells la variable x no se establece después de la canalización y ni siquiera durante la segunda mitad de la canalización.
¿Qué puedo hacer para contar las líneas de un oleoducto y luego actuar en función de ese número? Me gustaría evitar archivos temporales.
Respuesta1
este comentarioes verdad:
Cada parte de una tubería se inicia independientemente de las otras partes de la misma tubería. Esto significa que
$x
no puede estar disponible en medio del proceso si está configurado en una de las otras etapas.
Esto no significa que no puedas hacer nada. Una tubería puede considerarse el canal de datos principal, aún así los procesos pueden comunicarse usando canales laterales: archivos, llamados fifos o lo que sea (aunque a veces hay que tener mucho cuidado y no dejar que se bloqueen).
Desea contar el número de líneas y procesar condicionalmente todo el flujo de datos más adelante. Esto significa que debe llegar al final de la transmisión y solo entonces pasar toda la transmisión. Entonces necesitas guardar toda la transmisión de alguna manera. Un archivo temporal parece un enfoque sensato. Debes dividir tu pipa en al menos dos partes. La primera parte debería guardar los datos en un archivo; luego se deben contar las líneas (creo que esta tarea puede pertenecer a la primera parte); luego la última parte debe obtener el número, leer el archivo para recibir los datos desde el principio y actuar en consecuencia.
Si realmente desea evitar archivos temporales, entonces alguna parte de su canalización debería actuar de alguna manera como sponge
. Para evitar canales laterales, el número de líneas debe pasarse como la primera línea de salida y la parte restante de la tubería debe comprender este protocolo.
Considere este comando:
sed '$ {=; H; g; p;}; H; d'
Acumula líneas en un espacio de espera. Si hay al menos una línea, después de recibir la última línea sed
se imprime el número de líneas seguidas de una línea vacía y la entrada real.
La línea vacía es innecesaria pero aparece "naturalmente" en este código simple. En lugar de intentar evitarlo en sed
, simplemente lo abordaría más adelante en la tubería (por ejemplo, con sed '2 d'
).
Uso de ejemplo:
#!/bin/sh
sed '$ {=; H; g; p;}; H; d' | sed '2 d' | {
if ! IFS= read -r nlines; then
echo "0 lines. Nothing to do." >&2
else
echo "$nlines lines. Processing accordingly." >&2
if [ "$nlines" -ge 2 ]; then
grep a
else
grep b
fi
fi
}
Notas:
IFS= read -r
es una exageración porque la primera línea está bien definida y contiene un único número (o no existe).- Solía
/bin/sh
. El código también se ejecutará en Bash. No puede asumir
sed
que puede contener una cantidad arbitraria de datos.especificación POSIXdice:El patrón y los espacios de retención deberán poder contener cada uno al menos 8192 bytes.
Por lo tanto, puede ser que el límite sea de solo 8192 bytes. Por otro lado, puedo imaginar fácilmente un archivo temporal que contenga 1 TB de datos. Quizás no evite los archivos temporales a toda costa.
El título dice "cuenta el número de líneas", pero tu ejemplo intenta decidir si el número es 2 o más (N o más en general). Estos problemas no son equivalentes. Después de la segunda (enésima) línea de entrada, sabrá la respuesta al último problema; las líneas pares aparecerán indefinidamente. El código anterior no puede manejar entradas indefinidas. Arreglemoslo hasta cierto punto.
sed '
7~1 {p; d}
6 {H; g; i \
6+
p; d}
$ {=; H; g; p}
6! {H; d}
'
Este comando se comporta como la solución anterior, excepto que cuando llega a la sexta línea asume (imprime) que el número de líneas es 6+
. Luego, las líneas ya vistas se imprimen y las siguientes líneas (si las hay) se imprimen tan pronto como aparecen ( cat
comportamiento similar a).
Uso de ejemplo:
#!/bin/sh
threshold=6
sed "
$((threshold+1))~1 {p; d}
$threshold {H; g; i \
$threshold+
p; d}
$ {=; H; g; p}
${threshold}! {H; d}
" | sed '2 d' | {
if ! IFS= read -r nlines; then
echo "0 lines. Nothing to do." >&2
else
echo "$nlines lines. Processing accordingly." >&2
if [ "$nlines" = "$threshold+" ]; then
grep a
else
grep b
fi
fi
}
Notas:
- Se corrigió "hasta cierto punto" porque la limitación de
sed
(cualquiera que sea la limitación en su caso) aún se aplica. Pero ahorased
procesa como máximo$threshold
el número de líneas; Si$threshold
es lo suficientemente bajo, entonces debería estar bien. - El código de ejemplo solo realiza pruebas,
$threshold+
pero el protocolo le permite distinguir entre 0, 1, 2,…, umbral menos uno y umbral o más líneas.
No soy muy hábil en sed
. Si mi sed
código se puede simplificar, déjeme una pista en un comentario.
Respuesta2
Según la discusión y el código sed de Kamil, encontré esta solución extraña:
awk -v th="$threshold" '
function print_lines() { for (i in lines) print lines[i] }
NR < th { lines[NR] = $0 }
NR > th { print }
NR == th { print th; print_lines(); print }
END { if (NR < th) { print NR; print_lines(); } }' \
| if read nlines; then
if [ "$nlines" -eq "$threshold" ]; then
grep a
else
grep b
fi
fi