Correspondência de padrões em nomes de caminhos no bash

10

Eu quero atuar em uma lista de subdiretórios em um diretório. Considere:

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

Isso dá

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

Mas eu quero valores que correspondam à segunda parte do caminho, elf , gl , etc. Eu sei como remover o x86-headers .

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

que dá

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

Eu também sei como despir termos posteriores no caminho. Ou seja descendo um nível:

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

elf
gl
gmp
gnome2
gtk2
jni
libc

Mas tentar combinar isso não funciona. Ou seja,

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

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

Sem dúvida, isso é uma sintaxe incorreta, mas não sei a sintaxe correta. Claro que pode haver maneiras melhores de fazer isso. Se eu estivesse usando Python, usaria o split em / para dividir cada caminho em uma lista e, em seguida, escolheria o segundo elemento, mas não sei como fazer isso no bash.

EDIT: Obrigado pelas respostas. Eu também deveria ter perguntado, é possível fazer isso de forma portável e, em caso afirmativo, como?

    
por Faheem Mitha 01.02.2013 / 13:49

5 respostas

9

Você não pode combiná-los com bash (ou POSIXly), você precisa fazer isso em duas etapas.

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

Isso é portátil para qualquer shell POSIX. Se você precisa de portabilidade para o shell Bourne (mas por que você marcaria sua pergunta / bash ?), Caso você esteja migrando para o Solaris e forçado a usar /bin/sh em vez do padrão sh lá, ou portando para sistemas de 20 anos de idade), você poderia usar essa abordagem (que funcionará bem com shells POSIX):

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

(acima é um dos casos muito raros em que faz sentido deixar uma variável sem aspas).

Apenas para o registro, com zsh:

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

(o t ail da h ead do h ead do arquivo).

Ou para a sua abordagem python :

x=elf/C/populate.sh
i=${${(s:/:)x}[2]}
    
por 01.02.2013 / 13:55
7

A expansão de parâmetros não permite aninhar expressões, por isso você é forçado a fazer isso em duas etapas, com uma atribuição:

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

Você tem a opção de usar matrizes aqui, mas eu acho que seria mais complicado do que o PE acima:

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

Depois, há a terceira opção de usar comandos externos. Com uma pequena lista de arquivos, esta é geralmente a opção menos eficiente, mas será dimensionada da melhor maneira à medida que o número de arquivos cresce:

# For clarity, assume no names have linebreaks in them
find . -name populate.sh | cut -d / -f 3
    
por 01.02.2013 / 13:55
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
    
por 01.02.2013 / 14:07
2

Tenha muito cuidado ao usar um constructo como abaixo:

# 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 você pode acabar fazendo um echo * . Nesse caso, é apenas engraçado, mas pode ser muito pior se você for root, seu CWD é / e você deseja excluir alguns subdiretórios de /tmp em vez de fazer eco deles!

Para corrigir isso no bash, você poderia fazer:

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 o shopt -u nullglob no final para restaurar o comportamento padrão do bash após o loop.

Uma solução mais portátil poderia ser:

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

(As substituições dos parâmetros ## e %% são válidas em ksh88 ).

Siga este link para uma discussão mais completa sobre nullglob .

Observe que as 3 soluções acima falharão se você tiver um diretório intermediário com um espaço em seu nome. Para resolvê-lo, use aspas duplas na substituição de variável:

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

Para fazer correspondência de padrões em nomes de caminho portavelmente, você pode definir a variável IFS shell como / e, em seguida, usar a expansão de parâmetros do shell.

Fazer tudo isso em um subshell ajuda a manter o ambiente do shell pai inalterado!

# 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
    
por 01.02.2013 / 16:32