Correspondance de modèle sur les noms de chemin dans bash

10

Je veux agir sur une liste de sous-répertoires dans un répertoire. Considérer:

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

Cela donne

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

Mais je veux des valeurs qui correspond à la seconde partie du chemin, elf, gl, etc. Je sais comment Supprimer le caractère x86-headers.

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

qui donne

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

Je sais également comment supprimer les termes ultérieurs du chemin. C'est à dire en descendant d'un niveau:

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

donne

elf
gl
gmp
gnome2
gtk2
jni
libc

Mais essayer de les combiner ne fonctionne pas. C'est à dire

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

donne

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

C'est sans doute une syntaxe incorrecte, mais je ne connais pas la syntaxe correcte. Bien sûr, il pourrait y avoir de meilleures façons de le faire. Si j'utilisais Python, j'utiliserais split on /pour décomposer chaque chemin en une liste, puis choisir le deuxième élément, mais je ne sais pas comment faire cela en bash.

EDIT: Merci pour les réponses. J'aurais également dû demander, est-il possible de le faire de manière portable et si oui, comment?

Faheem Mitha
la source

Réponses:

9

Vous ne pouvez pas les combiner avec bash(ou POSIXly), vous devez le faire en deux étapes.

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

C'est portable sur n'importe quel shell POSIX. Si vous avez besoin de portabilité vers le shell Bourne (mais pourquoi balisez-vous alors votre question / bash ?) Au cas où vous porteriez vers Solaris et seriez obligé d'utiliser à la /bin/shplace de la norme sh, ou de porter sur des systèmes vieux de 20 ans) , vous pouvez utiliser cette approche (qui fonctionnera également avec les shells POSIX):

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

(ci-dessus est l'un des très rares cas où il est logique de laisser une variable sans guillemets).

Juste pour mémoire, avec zsh:

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

(la t tous du h ead du h ead du fichier).

Ou pour votre pythonapproche:

x=elf/C/populate.sh
i=${${(s:/:)x}[2]}
Stéphane Chazelas
la source
Le point-virgule i=${x#*/}; i=${i%%/*}est facultatif, non?
Dimitre Radoulov
3
@DimitreRadoulov C'est facultatif mais certains shells comme certaines anciennes versions de ash/dash exécuteraient les affectations de droite à gauche (ce qui était le comportement du shell Bourne) et causeraient des problèmes dans ce cas.
Stéphane Chazelas
7

L'expansion des paramètres ne vous permet pas d'imbriquer des expressions, vous êtes donc obligé de le faire en deux étapes, avec une affectation:

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

Vous avez la possibilité d'utiliser des tableaux ici, mais je pense que ce serait plus lourd que le PE ci-dessus:

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

Ensuite, il y a la troisième option d'utiliser des commandes externes. Avec une courte liste de fichiers, c'est généralement l'option la moins efficace, mais elle évoluera au mieux à mesure que le nombre de fichiers augmente:

# For clarity, assume no names have linebreaks in them
find . -name populate.sh | cut -d / -f 3
kojiro
la source
Ce n'est pas le cas de tous les coquilles, cependant, zshpermet l'emboîtement.
Stéphane Chazelas
3
@StephaneChazelas est juste, mais cette question est balisée 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
la source
2

Soyez très prudent en utilisant une construction comme ci-dessous:

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

Comme vous pourriez finir par faire un echo *. Dans ce cas, ce n'est que drôle, mais cela pourrait être bien pire si vous êtes root, votre CWDest /et que vous souhaitez supprimer certains sous-répertoires /tmpau lieu de les répéter!

Pour corriger cela dans bash, vous pouvez faire:

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

Remarquez shopt -u nullglobà la fin pour restaurer le comportement bash par défaut après la boucle.

Une solution plus portable pourrait être:

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

(Les substitutions de paramètres ##et %%sont valides dans ksh88 ).

Suivez ce lien pour une discussion plus complète de nullglob .

Notez que les 3 solutions ci-dessus échouent si vous avez un répertoire intermédiaire avec un espace dans son nom. Pour le résoudre, utilisez des guillemets doubles dans la substitution de variable:

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

Pour effectuer une correspondance de modèle sur les noms de chemin de manière portable, vous pouvez définir la IFSvariable shell sur /puis utiliser l'expansion des paramètres shell.

Faire tout cela dans un sous-shell aide à garder l'environnement du shell parent inchangé!

# 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
la source