Forma compatible con POSIX para trabajar con una lista de nombres de archivo posiblemente con espacios en blanco

14

He visto guías de scripts de Bash que sugieren el uso de una matriz para trabajar con nombres de archivos que contienen espacios en blanco. Sin embargo, DashAsBinSh sugiere que las matrices no son portátiles, por lo que estoy buscando una forma de trabajar compatible con POSIX con listas de nombres de archivos que pueden contener espacios en blanco.

Estoy buscando modificar el script de ejemplo a continuación para que pueda echo

foo/target/a.jar
foo/target/b.jar
bar/target/lol whitespace.jar

Aquí está el guión

#!/usr/bin/env sh

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"
# this would be produced by a 'ls' command
# We can execute the ls within the script, if it helps

dostuffwith() { echo $1; };

F_LOCATIONS=$INPUT
ALL_FILES=$(for f in $F_LOCATIONS; do echo `basename $f`; done)
ALL_FILES=$(echo "$ALL_FILES" | sort | uniq)

for f in $ALL_FILES
do
    fpath=$(echo "$F_LOCATIONS" | grep -m1 $f)
    dostuffwith $fpath
done
Eero Aaltonen
fuente
Posible lo mismo en SO: stackoverflow.com/questions/6499486/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Respuestas:

8

Conchas POSIX tienen una matriz: los parámetros posicionales ( $1, $2, etc., refered colectivamente como "$@").

set -- 'foo/target/a.jar' 'foo/target/b.jar' 'bar/target/b.jar' 'bar/target/lol whitespace.jar'
set -- "$@" '/another/one at the end.jar'

for jar do
  dostuffwith "$jar"
done

Esto es inconveniente porque solo hay uno y destruye cualquier otro uso de los parámetros posicionales. Los parámetros posicionales son locales para una función, que a veces es una bendición y otras una maldición.

Si se garantiza que los nombres de sus archivos no contienen nuevas líneas, puede usar nuevas líneas como separador. Cuando expande la variable, primero desactive el globbing con set -fy configure la lista de caracteres de división de campo IFSpara que contenga solo una nueva línea.

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"

set -f; IFS='
'                           # turn off variable value expansion except for splitting at newlines
for jar in $INPUT; do
  set +f; unset IFS
  dostuffwith "$jar"        # restore globbing and field splitting at all whitespace
done
set +f; unset IFS           # do it again in case $INPUT was empty

Con elementos en su lista separados por nuevas líneas, puede usar muchos comandos de procesamiento de texto de manera útil, en particular sort.

Recuerde siempre poner comillas dobles alrededor de las sustituciones de variables, excepto cuando desee explícitamente que se produzca la división de campos (así como el bloqueo, a menos que lo haya desactivado).

Gilles 'SO- deja de ser malvado'
fuente
Buena respuesta y explicación. Voy a marcar esto como aceptado porque hace que el sort | uniqpaso original funcione según lo previsto.
Eero Aaltonen
5

Como su $INPUTvariable usa líneas nuevas como separadores, voy a suponer que sus archivos no tendrán líneas nuevas en los nombres. Como tal, sí, hay una manera simple de iterar sobre los archivos y preservar los espacios en blanco.

La idea es usar el readshell incorporado. Normalmente readse dividirá en cualquier espacio en blanco, por lo que los espacios lo dividirán. Pero puede configurar IFS=$'\n'y se dividirá en líneas nuevas solamente. Para que pueda iterar sobre cada línea en su lista.

Aquí está la solución más pequeña que se me ocurrió:

INPUT="foo/target/a.jar
foo/target/b.jar
bar/target/b.jar
bar/target/lol whitespace.jar"

dostuffwith() {
    echo "$1"
}

echo "$INPUT" | awk -F/ '{if (!seen[$NF]++) print }' | \
while IFS=$'\n' read file; do
  dostuffwith "$file"
done

Básicamente, envía "$ INPUT" a la awkcual se deduplica el nombre del archivo (se divide /y luego imprime la línea si el último elemento no se ha visto antes). Luego, una vez que awk ha generado la lista de rutas de archivos, usamos while readpara recorrer la lista.

Patricio
fuente
$ checkbashisms bar.sh posible bashism en bar.sh línea 14 (<<< aquí cadena)
Eero Aaltonen
1
@EeroAaltonen Lo cambió para no usar la herejía. Sin embargo while, dostuffwithtenga en cuenta que con este cambio, el bucle y, por lo tanto, se ejecuta en una subshell. Por lo tanto, cualquier variable o cambio realizado en el shell en ejecución se perderá cuando se complete el ciclo. La única alternativa es usar un heredoc completo, que no es tan desagradable, pero pensé que sería preferible.
Patrick
Estoy otorgando puntos basados ​​más en la legibilidad que en la pequeñez. Esto ciertamente funciona y ya es +1 para eso.
Eero Aaltonen
IFS="\n"se divide en barra invertida yn caracteres. Pero en read file, no hay división. IFS="\n"sigue siendo útil porque elimina los caracteres en blanco de $ IFS que, de lo contrario, se habrían eliminado al principio y al final de la entrada. Para leer una línea, la sintaxis canónica es IFS= read -r line, sin embargo IFS=anything read -r line(siempre que todo lo que no contenga espacios en blanco) funcione también.
Stéphane Chazelas
¡Uy! No estoy seguro de cómo lo logré. Fijo.
Patrick