Comment puis-je développer un Tilde ~ dans le cadre d'une variable?

12

Lorsque j'ouvre une invite bash et tape:

$ set -o xtrace
$ x='~/someDirectory'
+ x='~/someDirectory'
$ echo $x
+ echo '~/someDirectory'
~/someDirectory

J'espérais que la 5ème ligne ci-dessus aurait disparu + echo /home/myUsername/someDirectory. Y a-t-il un moyen de faire cela? Dans mon script Bash d'origine, la variable x est en fait remplie à partir des données d'un fichier d'entrée, via une boucle comme celle-ci:

while IFS= read line
do
    params=($line)
    echo ${params[0]}
done <"./someInputFile.txt"

Pourtant, j'obtiens un résultat similaire, avec le echo '~/someDirectory'au lieu de echo /home/myUsername/someDirectory.

Andrew
la source
En ZSH, c'est x='~'; print -l ${x} ${~x}. J'ai abandonné après avoir fouillé le bashmanuel pendant un certain temps.
thrig
@thrig: Ce n'est pas un bashisme, ce comportement est POSIX.
WhiteWinterWolf du
Extrêmement proche (sinon un dupe): unix.stackexchange.com/questions/151850/…
Kusalananda
@Kusalananda: Je ne suis pas sûr que ce soit une dupe car la raison ici est quelque peu différente: l'OP n'a pas inclus $ x entre guillemets lors de son écho.
WhiteWinterWolf
Vous ne mettez pas de tildes dans le fichier d'entrée. Problème résolu.
chepner

Réponses:

11

La norme POSIX impose que l'expansion des mots se fasse dans l'ordre suivant (c'est moi qui souligne):

  1. L'expansion Tilde (voir Expansion Tilde), l' expansion des paramètres (voir Expansion des paramètres), la substitution de commande (voir Substitution de commande) et l'expansion arithmétique (voir Expansion arithmétique) doivent être effectuées, du début à la fin. Voir l'élément 5 dans la reconnaissance des jetons.

  2. Le fractionnement de champ (voir Découpage de champ) doit être effectué sur les parties des champs générés par l'étape 1, sauf si IFS est nul.

  3. L'extension de nom de chemin (voir Expansion de nom de chemin) doit être effectuée, à moins que l'ensemble -f ne soit en vigueur.

  4. La suppression de devis (voir Suppression de devis) doit toujours être effectuée en dernier.

Le seul point qui nous intéresse ici est le premier: comme vous pouvez le voir, l'expansion du tilde est traitée avant l'expansion des paramètres:

  1. Le shell tente une expansion de tilde echo $x, il n'y a pas de tilde à trouver, alors il continue.
  2. Le shell tente une expansion des paramètres echo $x, $xest trouvé et développé et la ligne de commande devient echo ~/someDirectory.
  3. Le traitement continue, l'expansion du tilde ayant déjà été traitée, le ~personnage reste tel quel.

En utilisant les guillemets lors de l'attribution du $x, vous demandiez explicitement de ne pas développer le tilde et de le traiter comme un caractère normal. Une chose souvent manquée est que dans les commandes shell, vous n'avez pas à citer toute la chaîne, vous pouvez donc faire en sorte que l'expansion se produise pendant l'affectation des variables:

user@host:~$ set -o xtrace
user@host:~$ x=~/'someDirectory'
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Et vous pouvez également faire en sorte que l'expansion se produise sur la echoligne de commande tant qu'elle peut se produire avant l' expansion des paramètres:

user@host:~$ x='someDirectory'
+ x=someDirectory
user@host:~$ echo ~/$x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Si, pour une raison quelconque, vous devez vraiment affecter le tilde à la $xvariable sans expansion et pouvoir le développer à la echocommande, vous devez procéder deux fois pour forcer deux extensions de la $xvariable à se produire:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ echo "$( eval echo $x )"
++ eval echo '~/someDirectory'
+++ echo /home/user/someDirectory
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Cependant, sachez que selon le contexte dans lequel vous utilisez une telle structure, cela peut avoir un effet secondaire indésirable. En règle générale, préférez éviter d'utiliser quoi que ce soit nécessitant evallorsque vous avez un autre moyen.

Si vous souhaitez traiter spécifiquement le problème du tilde par opposition à tout autre type d'expansion, une telle structure serait plus sûre et portable:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ case "$x" in "~/"*)
>     x="${HOME}/${x#"~/"}"
> esac
+ case "$x" in
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Cette structure vérifie explicitement la présence d'un interligne ~et le remplace par le répertoire home de l'utilisateur s'il est trouvé.

À la suite de votre commentaire, cela x="${HOME}/${x#"~/"}"peut en effet être surprenant pour quelqu'un qui n'est pas utilisé dans la programmation shell, mais est en fait lié à la même règle POSIX que j'ai citée ci-dessus.

Comme imposé par la norme POSIX, la suppression des devis se produit en dernier et l'expansion des paramètres se produit très tôt. Ainsi, ${#"~"}est évalué et développé bien avant l'évaluation des citations externes. À tour de rôle, comme défini dans les règles d' extension des paramètres :

Dans chaque cas où une valeur de mot est nécessaire (en fonction de l'état du paramètre, comme décrit ci-dessous), le mot doit être soumis à l'expansion tilde, à l'expansion des paramètres, à la substitution de commande et à l'expansion arithmétique.

Ainsi, le côté droit de l' #opérateur doit être correctement cité ou échappé pour éviter l'expansion du tilde.

Donc, pour le dire différemment, lorsque l'interpréteur de shell regarde x="${HOME}/${x#"~/"}", il voit:

  1. ${HOME}et ${x#"~/"}doit être étendu.
  2. ${HOME}est étendu au contenu de la $HOMEvariable.
  3. ${x#"~/"}déclenche une expansion imbriquée: "~/"est analysé mais, étant cité, est traité comme un littéral 1 . Vous auriez pu utiliser des guillemets simples ici avec le même résultat.
  4. ${x#"~/"}expression elle-même est désormais développée, ce qui entraîne la ~/suppression du préfixe de la valeur de $x.
  5. Le résultat de ce qui précède est maintenant concaténé: l'expansion de ${HOME}, le littéral /, l'expansion ${x#"~/"}.
  6. Le résultat final est entouré de guillemets doubles, empêchant fonctionnellement la division des mots. Je dis fonctionnellement ici parce que ces guillemets doubles ne sont pas techniquement nécessaires (voir ici et par exemple), mais en tant que style personnel dès qu'une affectation va au-delà, a=$bje trouve généralement plus clair d'ajouter des guillemets doubles.

Soit dit en passant, si vous regardez de plus près la casesyntaxe, vous verrez la "~/"*construction qui repose sur le même concept que x=~/'someDirectory'j'ai expliqué ci-dessus (ici encore, des guillemets simples et doubles pourraient être utilisés de manière interchangeable).

Ne vous inquiétez pas si ces choses peuvent sembler obscures à première vue (peut-être même à la seconde vue ou plus tard!). À mon avis, l'expansion des paramètres est, avec les sous-coquilles, l'un des concepts les plus complexes à comprendre lors de la programmation en langage shell.

Je sais que certaines personnes peuvent être en désaccord vigoureux, mais si vous souhaitez apprendre la programmation shell plus en profondeur, je vous encourage à lire le Guide de script avancé Bash : il enseigne le script Bash, donc avec beaucoup d'extensions et de cloches-et- sifflets par rapport aux scripts shell POSIX, mais je l'ai trouvé bien écrit avec beaucoup d'exemples pratiques. Une fois que vous gérez cela, il est facile de vous limiter aux fonctionnalités POSIX lorsque vous en avez besoin, je pense personnellement qu'entrer directement dans le domaine POSIX est une courbe d'apprentissage abrupte inutile pour les débutants (comparer mon remplacement de tilde POSIX avec Bash de type regex @ m0dular équivalent pour avoir une idée de ce que je veux dire;)!).


1 : Ce qui m'amène à trouver un bug dans Dash qui n'implémente pas correctement l'extension tilde ici (vérifiable à l'aide x='~/foo'; echo "${x#~/}"). L'extension des paramètres est un domaine complexe tant pour l'utilisateur que pour les développeurs de shell eux-mêmes!

WhiteWinterWolf
la source
Comment le shell bash analyse la ligne x="${HOME}/${x#"~/"}"? Il ressemble à une concaténation de 3 chaînes: "${HOME}/${x#", ~/et "}". Le shell autorise-t-il les guillemets doubles imbriqués lorsque la paire intérieure de guillemets doubles se trouve à l'intérieur d'un ${ }bloc?
Andrew
@Andrew: J'ai complété ma réponse avec des informations supplémentaires en espérant répondre à votre commentaire.
WhiteWinterWolf
Merci, c'est une excellente réponse. J'ai appris une tonne en le lisant. J'aimerais pouvoir voter plus d'une fois :)
Andrew
@WhiteWinterWolf: toujours, le shell ne voit pas les guillemets imbriqués quel que soit le résultat.
avp
6

Une réponse possible:

eval echo "$x"

Puisque vous lisez l'entrée d'un fichier, je ne le ferais pas.

Vous pouvez rechercher et remplacer le ~ par la valeur de $ HOME, comme ceci:

x='~/.config'
x="${x//\~/$HOME}"
echo "$x"

Donne moi:

/home/adrian/.config
m0dular
la source
Notez que l' ${parameter/pattern/string}extension est une extension Bash et peut ne pas être disponible dans d'autres shells.
WhiteWinterWolf
Vrai. Le PO a mentionné qu'il utilisait Bash, donc je pense que c'est une réponse appropriée.
m0dular
Je suis d'accord, tant que l'on s'en tient à Bash, pourquoi ne pas en profiter pleinement (tout le monde n'a pas besoin de portabilité partout), mais cela vaut la peine de le noter pour les utilisateurs non-Bash (plusieurs distributions sont désormais livrées avec Dash au lieu de Bash par exemple) ), les utilisateurs concernés ne sont donc pas surpris.
WhiteWinterWolf
J'ai pris la liberté de mentionner votre article dans ma digression sur les différences entre les extensions Bash et les scripts shell POSIX, car je pense que votre déclaration de type regex sur une seule ligne Bash par rapport à ma casestructure POSIX illustre bien comment les scripts Bash sont plus conviviaux, spécialement pour les débutants.
WhiteWinterWolf