¿Cómo contar POSIX-ly el número de líneas en una variable de cadena?

10

Sé que puedo hacer esto en Bash:

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

Básicamente, todo lo que encontré involucraba al <<<operador de Bash.

Pero en el shell POSIX, <<<no está definido, y no he podido encontrar un enfoque alternativo durante horas. Estoy bastante seguro de que hay una solución simple para esto, pero desafortunadamente, no la encontré hasta ahora.

LinuxSecurityFreak
fuente

Respuestas:

11

La respuesta simple es que wc -l <<< "${string_variable}"es un atajo de ksh / bash / zsh para printf "%s\n" "${string_variable}" | wc -l.

En realidad, hay diferencias en la forma <<<y el trabajo de una tubería: <<<crea un archivo temporal que se pasa como entrada al comando, mientras que |crea una tubería. En bash y pdksh / mksh (pero no en ksh93 o zsh), el comando en el lado derecho de la tubería se ejecuta en una subshell. Pero estas diferencias no importan en este caso particular.

Tenga en cuenta que, en términos de recuento de líneas, esto supone que la variable no está vacía y no termina con una nueva línea. El caso que no termina con una nueva línea es cuando la variable es el resultado de una sustitución de comando, por lo que obtendrá el resultado correcto en la mayoría de los casos, pero obtendrá 1 para la cadena vacía.

Hay dos diferencias entre var=$(somecommand); wc -l <<<"$var"y somecommand | wc -l: el uso de una sustitución de comando y una variable temporal elimina las líneas en blanco al final, olvida si la última línea de salida terminó en una nueva línea o no (siempre lo hace si el comando genera un archivo de texto no vacío válido) , y cuenta de más en uno si la salida está vacía. Si desea preservar el resultado y contar las líneas, puede hacerlo agregando un texto conocido y quitándolo al final:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"
Gilles 'SO- deja de ser malvado'
fuente
1
@Inian Keeping wc -les exactamente equivalente al original: <<<$fooagrega una nueva línea al valor de $foo(incluso si $fooestaba vacío). Explico en mi respuesta por qué esto puede no haber sido lo que se quería, pero es lo que se preguntó.
Gilles 'SO- deja de ser malvado'
2

No conforme a las funciones integradas de shell, utilizando utilidades externas como grepy awkcon opciones compatibles con POSIX,

string_variable="one
two
three
four"

Hacer greppara que coincida con el inicio de líneas

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

Y con awk

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

Tenga en cuenta que algunas de las herramientas de GNU, especialmente, GNU grepno respeta la POSIXLY_CORRECT=1opción de ejecutar la versión POSIX de la herramienta. En grepel único comportamiento afectado por el establecimiento de la variable será la diferencia en el procesamiento del orden de las banderas de línea de comando. De la documentación ( grepmanual de GNU ), parece que

POSIXLY_CORRECT

Si se establece, grep se comporta como lo requiere POSIX; de lo contrario, se grepcomporta más como otros programas de GNU. POSIX requiere que las opciones que siguen a los nombres de archivo se traten como nombres de archivo; de manera predeterminada, estas opciones están permutadas al principio de la lista de operandos y se tratan como opciones.

Consulte ¿Cómo usar POSIXLY_CORRECT en grep?

Inian
fuente
2
¿Seguramente wc -ltodavía es viable aquí?
Michael Homer
@MichaelHomer: Por lo que he observado, wc -lnecesita una secuencia adecuada delimitada por una nueva línea (que tiene un '\ n` al final para contar correctamente). Uno no puede usar un FIFO simple para usar printf, por ejemplo, printf '%s' "${string_variable}" | wc -lpodría no funcionar como se esperaba, pero lo <<<haría debido al seguimiento \nagregado por la herejía
Inian
1
Eso era lo que printf '%s\n'estaba haciendo, antes de sacarlo ...
Michael Homer
1

La cadena aquí <<<es más o menos una versión de una línea del documento aquí <<. La primera no es una característica estándar, pero la última sí. También puedes usarlo <<en este caso. Estos deberían ser equivalentes:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Sin embargo, tenga en cuenta que ambos agregan una nueva línea adicional al final de $somevar, por ejemplo, esto imprime 6, a pesar de que la variable solo tiene cinco líneas:

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

Con printf, puede decidir si desea la nueva línea adicional o no:

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

Pero luego, tenga en cuenta que wcsolo cuenta las líneas completas (o el número de caracteres de nueva línea en la cadena). grep -c ^También debe contar el fragmento de línea final.

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

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

(Por supuesto, también puede contar las líneas por completo en el shell utilizando la ${var%...}expansión para eliminarlas una a la vez en un bucle ...)

ilkkachu
fuente
0

En esos casos sorprendentemente frecuentes donde lo que realmente necesita hacer es procesar todas las líneas no vacías dentro de una variable de alguna manera (incluido contarlas), puede configurar IFS en solo una nueva línea y luego usar el mecanismo de división de palabras del shell para romper Las líneas no vacías separadas.

Por ejemplo, aquí hay una pequeña función de shell que totaliza las líneas no vacías dentro de todos los argumentos proporcionados:

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

Aquí se usan paréntesis, en lugar de llaves, para formar el comando compuesto para el cuerpo de la función. Esto hace que la función se ejecute en un subshell para que no contamine la configuración de expansión de nombre de ruta y variable IFS del mundo exterior en cada llamada.

Si desea iterar sobre líneas no vacías, puede hacerlo de manera similar:

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

Manipular IFS de esta manera es una técnica que a menudo se pasa por alto, también útil para hacer cosas como analizar nombres de ruta que podrían contener espacios desde entradas en columnas delimitadas por tabuladores. Sin embargo, debe ser consciente de que eliminar deliberadamente el carácter de espacio generalmente incluido en la configuración predeterminada de IFS de espacio-tabulación-nueva línea puede terminar deshabilitando la división de palabras en lugares donde normalmente esperaría verlo.

Por ejemplo, si está utilizando variables para construir una línea de comando complicada para algo así ffmpeg, es posible que desee incluir -vf scale=$scalesolo cuando la variable scalese establece en algo que no está vacío. Normalmente, podría lograr esto con, ${scale:+-vf scale=$scale}pero si IFS no incluye su carácter de espacio habitual en el momento en que se realiza esta expansión de parámetros, el espacio entre -vfy scale=no se utilizará como un separador de palabras y se ffmpegpasará todo -vf scale=$scalecomo un argumento único, que no entenderá

Para solucionar esto, puede que sea necesario para asegurarse de que IFS se fijó de manera más normal antes de hacer la ${scale}expansión, o hacer dos expansiones: ${scale:+-vf} ${scale:+scale=$scale}. La división de la palabra que hace el shell en el proceso de análisis inicial de las líneas de comando, a diferencia de la división que realiza durante la fase de expansión del procesamiento de esas líneas de comando, no depende de IFS.

Otra cosa que podría valer la pena si va a hacer este tipo de cosas sería crear dos variables de shell globales para contener solo una pestaña y una nueva línea:

t=' '
n='
'

De esa manera, puede incluir $ty $nen expansiones donde necesita pestañas y nuevas líneas, en lugar de ensuciar todo su código con espacios en blanco entre comillas. Si prefiere evitar los espacios en blanco citados por completo en un shell POSIX que no tiene otro mecanismo para hacerlo, printfpuede ayudar, aunque necesita un poco de violín para evitar la eliminación de nuevas líneas en las expansiones de comandos:

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

A veces, configurar IFS como si fuera una variable de entorno por comando funciona bien. Por ejemplo, aquí hay un bucle que lee un nombre de ruta que puede contener espacios y un factor de escala de cada línea de un archivo de entrada delimitado por tabulaciones:

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

En este caso, la readversión integrada ve el IFS establecido en solo una pestaña, por lo que no dividirá la línea de entrada que lee en los espacios también. Pero IFS=$t set -- $lines no funciona: el shell se expande a $linesmedida que construye los setargumentos incorporados antes de ejecutar el comando, por lo que la configuración temporal de IFS de una manera que se aplica solo durante la ejecución del propio desarrollo llega demasiado tarde. Esta es la razón por la cual los fragmentos de código que he dado sobre todo establecen IFS en un paso separado, y por qué tienen que lidiar con el problema de preservarlo.

Flabdablet
fuente