Coincidencia de patrones en nombres de ruta en bash

10

Quiero actuar en una lista de subdirectorios en un directorio. Considerar:

for x in x86-headers/*/C/populate.sh; do echo $x; done

Esto da

x86-headers/elf/C/populate.sh
x86-headers/gl/C/populate.sh
x86-headers/gmp/C/populate.sh
x86-headers/gnome2/C/populate.sh
x86-headers/gtk2/C/populate.sh
x86-headers/jni/C/populate.sh
x86-headers/libc/C/populate.sh

Pero quiero valores que corresponde a la segunda parte de la ruta, elf, gl, etc. Sé cómo quitarse el principal x86-headers.

for x in x86-headers/*/C/populate.sh; do i=${x##x86-headers/}; echo $i; done

lo que da

elf/C/populate.sh
gl/C/populate.sh
gmp/C/populate.sh
gnome2/C/populate.sh
gtk2/C/populate.sh
jni/C/populate.sh
libc/C/populate.sh

También sé cómo quitar los términos posteriores en el camino. Es decir, bajando un nivel:

cd x86-headers
for x in */C/populate.sh; do i=${x%%/*}; echo $i; done

da

elf
gl
gmp
gnome2
gtk2
jni
libc

Pero, tratar de combinar estos no funciona. Es decir

for x in x86-headers/*/C/populate.sh; do i=${${x##x86-headers}%%/*}; echo $i; done

da

bash: ${${x##x86-headers}%%/*}: bad substitution

Sin duda, esta es una sintaxis incorrecta, pero no sé la sintaxis correcta. Por supuesto, podría haber mejores formas de hacer esto. Si usara Python, usaría dividir /para dividir cada ruta en una lista y luego elegir el segundo elemento, pero no sé cómo hacerlo en bash.

EDITAR: Gracias por las respuestas. También debería haber preguntado, ¿es posible hacer esto de forma portátil, y si es así, cómo?

Faheem Mitha
fuente

Respuestas:

9

No puede combinarlos con bash(o POSIXly), debe hacerlo en dos pasos.

i=${x#*/}; i=${i%%/*}

Eso es portátil para cualquier shell POSIX. Si necesita portabilidad para el shell Bourne (pero ¿por qué etiquetaría su pregunta / bash entonces?) También en caso de que esté transfiriendo a Solaris y se vea obligado a usarlo en /bin/shlugar del estándar shallí, o transfiriendo a sistemas de 20 años) , podría utilizar este enfoque (que funcionará también con shells POSIX):

IFS=/; set -f
set x $x
i=$3

(arriba es uno de los casos muy raros en los que tiene sentido dejar una variable sin comillas).

Solo para el registro, con zsh:

print -rl -- x86-headers/*/C/populate.sh(:h:h:t)

(la t ail de la h ead de la h ea del archivo).

O por su pythonenfoque:

x=elf/C/populate.sh
i=${${(s:/:)x}[2]}
Stéphane Chazelas
fuente
El punto y coma i=${x#*/}; i=${i%%/*}es opcional, ¿verdad?
Dimitre Radoulov 01 de
3
@DimitreRadoulov Es opcional, pero algunos shells como algunas versiones antiguas de ash/dash ejecutarían las asignaciones de derecha a izquierda (que era cómo se comportaba el shell Bourne) y causan problemas en este caso.
Stéphane Chazelas 01 de
7

La expansión de parámetros no le permite anidar expresiones, por lo que se ve obligado a hacerlo en dos pasos, con una asignación:

i=${x##x86-headers/}
i=${i%%/*}

Tienes la opción de usar matrices aquí, pero creo que sería más engorroso que el PE anterior:

IFS=/
set -f # Disable globbing in case unquoted $x has globs in it
i=( $x )
set +f
echo "${i[1]}"

Luego está la tercera opción de usar comandos externos. Con una lista corta de archivos, esta suele ser la opción menos eficiente, pero escalará mejor a medida que aumente la cantidad de archivos:

# For clarity, assume no names have linebreaks in them
find . -name populate.sh | cut -d / -f 3
kojiro
fuente
Sin embargo, eso no es cierto para todos los depósitos, zshpermite el anidamiento.
Stéphane Chazelas
3
@StephaneChazelas es justo, pero esta pregunta está etiquetada bash.
kojiro
2
regex="x86-headers/([^/]*)/.*"
for f in x86-headers/*/C/populate.sh; do [[ $f =~ $regex ]] && echo "${BASH_REMATCH[1]}"; done
elf
gl
gmp
gnome2
gtk2
jni
libc
watael
fuente
2

Tenga mucho cuidado al usar una construcción como se muestra a continuación:

# What happen if no files match ?
for x in x86-headers/*/C/populate.sh; do
  x=${x##x86-headers/}
  x=${x%%/*}
  echo $x
done

Como podrás terminar haciendo un echo *. En este caso, solo es divertido, ¡pero podría ser mucho peor si eres root, your CWDis /y quieres eliminar algunos subdirectorios en /tmplugar de repetirlos!

Para arreglar eso en bash, puedes hacer:

shopt -s nullglob
for x in x86-headers/*/C/populate.sh; do
  x=${x##x86-headers/}
  x=${x%%/*}
  echo $x
done
shopt -u nullglob

Observe shopt -u nullglobal final para restaurar el comportamiento predeterminado de bash después del ciclo.

Una solución más portátil podría ser:

for x in x86-headers/*/C/populate.sh; do
  [ -e $x ] || break
  x=${x##x86-headers/}
  x=${x%%/*}
  echo $x
done

(Las sustituciones ##y los %%parámetros son válidos en ksh88 ).

Siga este enlace para una discusión más completa de nullglob .

Observe que las 3 soluciones anteriores fallan si tiene un directorio intermediario con un espacio en su nombre. Para resolverlo, use comillas dobles en la sustitución de variables:

for x in x86-headers/*/C/populate.sh; do
  [ -e "$x" ] || break
  x="${x##x86-headers/}"
  x="${x%%/*}"
  echo "$x"
done
jfg956
fuente
0

Para hacer una coincidencia de patrones en los nombres de ruta de forma portátil, puede configurar la IFSvariable de shell /y luego usar la expansión de parámetros de shell.

¡Hacer todo esto en un subshell ayuda a mantener el entorno del shell principal sin cambios!

# cf. "The real Bourne shell problem",
# http://utcc.utoronto.ca/~cks/space/blog/programming/BourneShellLists

paths='
x86-headers/elf/C/populate.sh
x86-headers/gl/C/populate.sh
x86-headers/gmp/C/populate.sh
x86-headers/gnome2/C/populate.sh
x86-headers/gtk2/C/populate.sh
x86-headers/jni/C/populate.sh
x86-headers/libc/C/populate.sh
'

IFS='
'

# version 1
for x in $paths; do 
   ( IFS='/'; set -- ${x}; echo "$2" ); 
done


# version 2
set -- ${paths}
for x in $@; do 
   ( IFS='/'; set -- ${x}; echo "$2" ); 
done
carlo
fuente