Como a expansão de nome de arquivo do shell delimita itens dentro de uma lista (*)?

5

Eu não entendo totalmente a expansão do shell ainda (espero que um dia em breve eu o faça) ...
Eu vi esse comentário para uma pergunta de superusuário , mas acho que ainda estou estacionado no meio-fio ...

Using Linux without the shell is like driving a Ferrari at 50 km/h through city traffic. All fun will just go away ...

Eu não entendo o seguinte exemplo. Qual hierarquia, ou o que quer que seja, está causando o segundo exemplo "array item count:" diferente do primeiro exemplo?

O que aconteceu com o shell introduzido "espaço"? ou é o echo que está introduzindo o espaço, e o shell está (talvez) usando \ 0?

#!/bin/bash
# Make a couple of files whose names contain a space.
junkd=$HOME/junkd
mkdir $junkd # || exit 1
cd $junkd
touch f\ {1..2}
#
echo -n * |xxd         # This shows a space between the two names.
names=$(echo -n * )
echo -n "$names" |xxd  # This shows a space between the two names.
#
# So far, it seems that the shell is inserting a space between each filename.
#
array=( $names )
echo "array item count: ${#array[@]}" 
# 4 items... This shows that a space is the delimiter char ....
#
array=( * )
echo "array item count: ${#array[@]}" 
# 2 items... What happened to the shell introduced space?
#
    
por Peter.O 11.03.2011 / 16:26

2 respostas

2

De man bash

EXPANSION Expansion is performed on the command line after it has been split into words. There are seven kinds of expansion performed: brace expansion, tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, word splitting, and pathname expansion. The order of expansions is: brace expansion, tilde expansion, parameter, variable and arithmetic expansion and command substitution (done in a left-to-right fashion), word splitting, and pathname expansion.

array=( $names )

O motivo de 4 entradas é porque um parâmetro $names sem aspas está sujeito a divisão de palavras com base no separador de campos interno IFS , que por padrão é <space><tab><newline> . Se você fosse citar "$names" para inibir a divisão de palavras, você só obteria um elemento de matriz com o valor f 1 f 2 , mais uma vez não o que você deseja.

array=( * )

O acima, por outro lado, está sujeito somente à expansão do nome do caminho , que é a última expansão realizada. Os resultados não estão sujeitos à divisão de palavras, assim você obtém os 2 elementos desejados.

Se você deseja fazer array=( $names ) work, precisará separar os nomes de arquivos de alguma forma por um caractere não-espacial que também não esteja contido nos nomes dos arquivos. Você precisará então configurar o IFS para esse caractere.

$ names=$(echo f* | sed "s/ /#/2")
$ echo $names
f 1#f 2
$ IFS='#' array=( $names )
$ echo ${#array[@]}
2
$ echo ${array[0]}
f 1

Uma maneira mais elegante de fazer isso seria usar o byte find do NUL como o delimitador de nome de arquivo, já que é garantido que ele nunca ficará separado de um nome de arquivo. Para fazer isso, precisaremos usar o comando -print0 com seu sinal read , bem como o IFS incorporado no NUL. Nós também precisamos limpar o IFS para que nenhuma divisão de palavras em espaços seja realizada.

#!/bin/bash

unset array

while IFS= read -r -d $'
$ touch f{1,2}; IFS="#"; rm f1#f2
rm: cannot remove 'f1#f2': No such file or directory
' name; do array+=( "$name" ) done < <(find . -type f -name "f*" -print0 )

Atualizar

Expansion is performed on the command line after it has been split into words.

Eu posso ver como alguém ficaria confuso com a citação acima apenas para afirmar que divisão de palavras é a segunda à última expansão a ocorrer.

Uma maneira melhor de traduzir essa frase na minha opinião seria:

Expansion is performed on the command line after it has been split into arguments.

A divisão de argumentos no shell é feita sempre pelo espaço em branco, e são esses argumentos que estão sujeitos a expansão. Se você quer ter espaço em branco em seu argumento, você deve usar Quoting ou Escapando . IFS não aumenta a divisão de argumentos, apenas a divisão de palavras.

Considere este exemplo:

array=( $names )

Observe como a configuração # to f1#f2 não alterou o fato de que o shell ainda só viu um argumento %code% ; que a propósito está então sujeito às várias expansões.

Eu recomendaria muito seu aquatint com o BashFAQ se você ainda não o fez. Em particular, sugiro strongmente que você leia as seguintes duas entradas suplementares:

  1. Argumentos
  2. Divisão de palavras
por 11.03.2011 / 18:17
5

Um comando shell (mais precisamente, um “comando simples”) consiste em uma lista de palavras. Cada palavra pode ser uma string arbitrária (uma palavra shell pode conter espaços e caracteres de pontuação).

Quando você executa echo -n * , o shell executa a expansão do nome do caminho (também chamado geração de nome de arquivo ou globbing) em * e o substitui pela lista de nomes de arquivos correspondentes. Portanto, após a expansão, esse comando consiste em quatro palavras: echo , -n , f 1 e f 2 . O comando echo é executado com dois argumentos e imprime seus argumentos com um espaço intermediário (e nenhuma nova linha de finalização por causa da opção -n ). Então a saída é f 1 f 2 . Exercício: crie outro arquivo cujo nome consista em dois espaços consecutivos, execute echo -n * e tenha certeza de entender a saída.

Quando você executa names=$(echo -n * ) , a saída do comando é armazenada na variável names . Aqui, essa linha é equivalente a names='f 1 f 2' .

Agora chegamos a array=( $names ) . Essa é uma atribuição de matriz, mas não afeta a expansão nesse caso. Como $names é uma expansão de variável sem aspas, ela está sujeita à divisão de palavras seguida pela expansão do nome do caminho. A divisão de palavras significa que o valor da variável (que é uma string) é dividido em partes em cada sequência de espaço em branco (para as regras precisas, procure IFS na documentação do seu shell). Você pode acabar com zero, uma ou mais palavras; aqui a string é dividida em 4 palavras: f , 1 , f e 2 . Portanto, o array contém quatro elementos (cada um com uma palavra de um caractere). Exercício: com esse arquivo extra com dois espaços consecutivos em seu nome, qual é o conteúdo exato da matriz?

Em seguida, você tentou array=( * ) . Aqui, há uma única palavra na matriz, sujeita às expansões usuais, a última das quais é a expansão do nome do caminho. Como existem dois arquivos correspondentes, a matriz contém duas palavras, os nomes de cada arquivo: f 1 e f 2 .

Em termos de prática de programação de shell, que conselho podemos extrair dessa análise? Bem, primeiro, há o princípio usual de programação de shell: sempre coloque aspas duplas em torno de expansões variáveis, a menos que você tenha uma boa razão para não fazer isso. Então, não armazene uma lista em uma variável de string. Se você quiser armazenar uma lista de nomes de arquivos, coloque-os diretamente em uma matriz:

files=(*)
ls -l "${files[@]}"

Exercício adicional: crie um arquivo cujo nome seja um único asterisco ( touch '*' ) e execute esses comandos novamente. Você entende a saída?

Além disso, o zsh não executa a divisão de palavras ou a expansão do nome de caminho nas expansões de variáveis. Isso faz com que seja um pouco melhor programar.

    
por 12.03.2011 / 01:15

Tags