Bash: les citations échappées dans le sous-shell

Lorsque j'exécute la command suivante:

#!/bin/bash while IFS= read -r -d '' file; do files+=$file done < <(find -type f -name '*.c' -print0) echo "${files[@]}" 

Je n'ai pas le même résultat que celui-ci:

 #!/bin/bash find_args="-type f '*.c' -print0" while IFS= read -r -d '' file; do files+=$file done < <(find $find_args) echo "${files[@]}" 

Comment puis-je résoudre le deuxième scénario pour être équivalent au premier?

Ma compréhension est que, parce qu'il y a des guillemets simples dans les guillemets, les guillemets simples s'échappent, ce qui produit une expansion incorrecte qui ressemble à quelque chose comme ça:

 find -type f -name ''\''*.c'\'' -print0 

(Notez que vous avez une faute de frappe. Vous avez laissé l'indicateur -name dans le deuxième exemple.)

Une approche consiste à mettre les arguments dans un tableau et à passer le tableau de façon appropriée pour find

 #!/bin/bash find_args=(-type f -name '*.c' -print0) while IFS= read -r -d '' file; do files+=$file done < <(find "${find_args[@]}") echo "${files[@]}" 

Le format ${foo[@]} s'étend à tous les éléments du tableau, chacun étant un mot individuel (au lieu d'être étendu à une seule string). C'est plus proche dans l'intention du script original.

La réponse de Blayer est correcte, mais pour débuild ce qui se passe vraiment ici (en ignorant la faute du nom manquant -name primaire):

 #!/bin/bash while IFS= read -r -d '' file; do files+=$file done < <(find -type f -name '*.c' -print0) echo "${files[@]}" 

Dans le shell lancé par la substitution de process ( <(...) ), la command suivante est analysée par bash:

 find -type f -name '*.c' -print0 

Parce que le glob *.c est cité, bash ne le développe pas. Cependant, les guillemets simples sont supprimés. Ainsi, lorsque le process de find commence, ce qu'il considère comme sa list d'arguments est:

 -type f -name *.c -print0 

Notez que ces arguments sont séparés par des octets NULL , et non par des espaces ou des returns à la ligne. C'est au niveau C, pas au niveau de la shell. Cela a à voir avec la façon dont les programmes sont exécutés en utilisant execve() dans C.

Maintenant, pour contraster, dans l'extrait suivant:

 #!/bin/bash find_args="-type f -name '*.c' -print0" while IFS= read -r -d '' file; do files+=$file done < <(find $find_args) echo "${files[@]}" 

La valeur de la variable find_args est définie sur:

 -type f -name '*.c' -print0 

(Les guillemets doubles ne font pas partie de la valeur, mais les guillemets simples le sont. )

Lorsque la command find $find_args est exécutée, le jeton $find_args est soumis à l'expansion des parameters, suivi de la division des mots, suivi de l' expansion du nom de domaine (aka glob expansion).

Après l'extension du paramètre, vous avez -type f -name '*.c' -print0 . Notez que c'est après la suppression de devis. Ainsi, les guillemets simples ne seront pas supprimés.

Après la division des mots, vous avez les mots suivants séparés:

 -type f -name '*.c' -print0 

Puis vient l'expansion de pathname. Bien sûr, '*.c' ne correspondra probablement à rien car vous ne mettez généralement pas de guillemets simples dans vos noms de files, donc le résultat sera probablement que '*.c' sera passé comme un model littéral à find , et ainsi, le nom -name primaire échouera sur tous les files. (Il ne réussirait que s'il y a un file dont le nom commence par un guillemet simple et se termine par les trois caractères .c' )


Edit: En fait, s'il y a un tel file, le glob '*.c' sera développé pour correspondre à ce file et à tous les autres files, puis l' extension [le nom réel du file] sera passé comme model. Donc, si le -print0 primary sera jamais atteint ou non, cela dépend de (a) s'il y a un seul nom de file, et (b) si ce nom de file, interprété comme un glob, correspond à lui-même.

Exemples:

Si vous exécutez, touch "'something.c'" , alors le glob '*.c' sera étendu à 'something.c' , puis la find primaire -name 'something.c' correspondra également à ce file. être imprimé.

Si vous exécutez touch "'namewithcharset[a].c'" , le glob '*.c' sera étendu à celui par le shell, mais la find -name 'namewithcharset[a].c' ne sera pas identique, il ne correspondrait qu'à 'namewithcharseta.c' , qui n'existe pas-donc -print0 ne serait pas atteint.

Si vous exécutez touch "'x.c'" "'y.c'" , le glob '*.c' sera étendu aux deux noms de files, ce qui provoquera une erreur à partir de find car 'y.c' n'est pas un valide primaire (et il ne peut pas être car il ne commence pas par un trait d'union).


Si l'option nullglob est définie, vous obtiendrez un comportement différent.

Voir également:

  • Pourquoi mon script shell se bloque-t-il sur des espaces ou d'autres caractères spéciaux?
  • Pourquoi boucler la mauvaise pratique de sortie de la découverte?

En plus de ce qui a déjà été dit, vous devez:

  • déclarer la variable $files tant que tableau car par défaut scalar et var+=something sur un scalaire fait une concaténation de string (ou une addition arithmétique si le scalaire a reçu l'atsortingbut entier ). Ou utilisez la syntaxe var+=(something) (qui convertirait automatiquement la variable en tableau).
  • initialiser la variable (en tant que non définie ou à une list vide), sinon vous pouvez hériter d'une valeur initiale de l'environnement.

Faire:

 files=() while ... files+=$file # or files+=("$file") done 

Cela suffirait à less que la variable de files ait déjà été déclarée en tant que tableau associatif plus tôt dans le script (dans ce cas, les files+=something ressemblerait à des files["0"]+=something et des files+=("$files") Erreur).

Si vous ne pouvez pas garantir que les files n'ont pas été définis en tant que tableau associatif plus tôt dans le script, vous devrez peut-être:

 typeset -a files=() 

au contraire, cela aurait l'effet secondaire de limiter la scope de la variable à la fonction englobante. typeset -ga files=() ne fonctionne pas correctement en tant que work around pour cela dans bash car il déclarerait la variable dans la scope globale. unset files; files=() unset files; files=() peut ne pas fonctionner car les unset files peuvent dans certains cas révéler la variable des files d'une scope externe (qui peut être un tableau associatif) au lieu de l'annuler.

Vous devez faire quelques changements dans le deuxième script. Insérez le nom-manquant, supprimez les guillemets simples et utilisez set -f pour éviter l'expansion de l'astérisque. Soit au début du script ou (comme indiqué) dans le sous-shell:

 <(set -f; find $find_args) 

changez l'echo en printf et faites l'assignation en tant que files+=("$file") pour avoir un tableau de valeurs. Ce que vous avez seulement construit une string.

 #!/bin/bash find_args="-type f -name *.c -print0" while IFS= read -r -d '' file; do files+=("$file") done < <(set -f; find $find_args) printf '<%s> ' "${files[@]}"; echo