Correspondência de padrões nos nomes de caminho no bash

10

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

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

Isto 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 corresponde à segunda parte do caminho, elf, gl, etc. Eu sei como retirar o líder 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 retirar 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, esta é uma sintaxe incorreta, mas não conheço a sintaxe correta. Claro que pode haver melhores maneiras de fazer isso. Se eu estivesse usando Python, usaria split on /para dividir cada caminho em uma lista e depois escolher 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? Se sim, como?

Faheem Mitha
fonte

Respostas:

9

Você não pode combiná-los com bash(ou POSIXly), você deve fazê-lo em duas etapas.

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

É portátil para qualquer shell POSIX. Se você precisar de portabilidade para o shell Bourne (mas por que você marcaria sua pergunta / bash ?) Também, caso esteja portando para o Solaris e forçado a usar em /bin/shvez do padrão shlá ou portando sistemas de 20 anos) , você pode usar esta abordagem (que também funcionará 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 constar, com zsh:

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

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

Ou para sua pythonabordagem:

x=elf/C/populate.sh
i=${${(s:/:)x}[2]}
Stéphane Chazelas
fonte
O ponto e vírgula em i=${x#*/}; i=${i%%/*}é opcional, certo?
Dimitre Radoulov
3
@DimitreRadoulov É opcional, mas alguns shells, como algumas versões antigas do ash/dash , executavam as atribuições da direita para a esquerda (como era o comportamento do shell Bourne) e causavam problemas nesse caso.
Stéphane Chazelas
7

A expansão de parâmetros não permite aninhar expressões; portanto, você é forçado a fazê-lo em duas etapas, com uma atribuição:

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

Você tem a opção de usar matrizes aqui, mas 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, essa geralmente é a opção menos eficiente, mas será dimensionada da melhor maneira que o número de arquivos aumentar:

# For clarity, assume no names have linebreaks in them
find . -name populate.sh | cut -d / -f 3
kojiro
fonte
Isso não é verdade para todas as conchas, porém, zshpermite o aninhamento.
Stéphane Chazelas
3
@StephaneChazelas justo o suficiente, mas esta pergunta está marcada 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
fonte
2

Tenha muito cuidado usando uma construção 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ê é root, CWDis is /e você deseja excluir alguns subdiretórios em /tmpvez de repeti-los!

Para corrigir isso no bash, você pode 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 nullglobno final para restaurar o comportamento padrão do bash após o loop.

Uma solução mais portátil pode 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 ##e de %%parâmetros são válidas em ksh88 ).

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

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

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

Para fazer a correspondência de padrões nos nomes dos caminhos de maneira portável, você pode definir a IFSvariável do shell como /e, em seguida, usar a expansão dos parâmetros do shell.

Fazer tudo isso de forma resumida 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
carlo
fonte