Comment développer manuellement une variable spéciale (ex: ~ tilde) dans bash

135

J'ai une variable dans mon script bash dont la valeur est quelque chose comme ceci:

~/a/b/c

Notez qu'il s'agit d'un tilde non développé. Quand je fais ls -lt sur cette variable (appelez-la $ VAR), je n'obtiens pas un tel répertoire. Je veux laisser bash interpréter / développer cette variable sans l'exécuter. En d'autres termes, je veux que bash exécute eval mais pas la commande évaluée. Est-ce possible en bash?

Comment ai-je réussi à transmettre cela dans mon script sans extension? J'ai passé l'argument en l'entourant de guillemets doubles.

Essayez cette commande pour voir ce que je veux dire:

ls -lt "~"

C'est exactement la situation dans laquelle je me trouve. Je veux que le tilde soit élargi. En d'autres termes, par quoi dois-je remplacer la magie pour rendre ces deux commandes identiques:

ls -lt ~/abc/def/ghi

et

ls -lt $(magic "~/abc/def/ghi")

Notez que ~ / abc / def / ghi peut exister ou non.

madiyaan damha
la source
4
L' expansion de Tilde entre guillemets peut également vous être utile. Cela évite la plupart du temps, mais pas entièrement, d'utiliser eval.
Jonathan Leffler
2
Comment votre variable a-t-elle été affectée avec un tilde non développé? Peut-être que tout ce qui est requis est d'assigner cette variable avec le tilde hors guillemets. foo=~/"$filepath"oufoo="$HOME/$filepath"
Chad Skeeters
dir="$(readlink -f "$dir")"
Jack Wasey le

Réponses:

98

En raison de la nature de StackOverflow, je ne peux pas simplement rendre cette réponse non acceptée, mais au cours des 5 années écoulées depuis que j'ai publié cela, il y a eu de bien meilleures réponses que ma réponse, certes rudimentaire et plutôt mauvaise (j'étais jeune, ne tuez pas moi).

Les autres solutions de ce fil sont des solutions plus sûres et meilleures. De préférence, j'irais avec l'un de ces deux:


Réponse originale à des fins historiques (mais veuillez ne pas l'utiliser)

Si je ne me trompe pas, "~"ne sera pas développé par un script bash de cette manière car il est traité comme une chaîne littérale "~". Vous pouvez forcer l'expansion via evalcomme ça.

#!/bin/bash

homedir=~
eval homedir=$homedir
echo $homedir # prints home path

Sinon, utilisez simplement ${HOME}si vous voulez le répertoire personnel de l'utilisateur.

wkl
la source
3
Avez-vous un correctif pour le moment où la variable contient un espace?
Hugo le
34
J'ai trouvé le ${HOME}plus attrayant. Y a-t-il une raison de ne pas en faire votre principale recommandation? En tout cas, merci!
sage
1
+1 - J'avais besoin de développer ~ $ some_other_user et eval fonctionne bien quand $ HOME ne fonctionnera pas car je n'ai pas besoin de la page d'accueil de l'utilisateur actuel.
olivecoder
12
L'utilisation evalest une suggestion horrible, c'est vraiment mauvais qu'elle reçoive autant de votes positifs. Vous rencontrerez toutes sortes de problèmes lorsque la valeur de la variable contient des méta caractères du shell.
user2719058
1
Je n'ai pas pu terminer mon commentaire à ce moment-là et, par conséquent, je n'ai pas été autorisé à le modifier plus tard. Je suis donc reconnaissant (merci encore @birryree) pour cette solution car elle a aidé dans mon contexte spécifique à ce moment-là. Merci Charles de m'avoir informé.
olivecoder
114

Si la variable varest entrée par l'utilisateur, neeval doit pas être utilisée pour développer le tilde en utilisant

eval var=$var  # Do not use this!

La raison est: l'utilisateur pourrait par accident (ou par but) taper par exemple var="$(rm -rf $HOME/)"avec des conséquences désastreuses possibles.

Une meilleure méthode (et plus sûre) consiste à utiliser l'expansion des paramètres Bash:

var="${var/#\~/$HOME}"
Håkon Hægland
la source
8
Comment pouvez-vous changer ~ userName / au lieu de juste ~ /?
aspergillusOryzae
3
Quel est le but de #in "${var/#\~/$HOME}"?
Jahid
4
@Jahid C'est expliqué dans le manuel . Cela force le tilde à ne correspondre qu'au début de $var.
Håkon Hægland
1
Merci. (1) Pourquoi avons-nous besoin de \~s'échapper ~? (2) Votre réponse suppose que ~c'est le premier caractère de $var. Comment pouvons-nous ignorer les principaux espaces blancs $var?
Tim
1
@Tim Merci pour le commentaire. Oui, vous avez raison, nous n'avons pas besoin d'échapper à un tilde sauf s'il s'agit du premier caractère d'une chaîne non entre guillemets ou s'il suit un :dans une chaîne non entre guillemets. Plus d'informations dans la documentation . Pour supprimer les espaces blancs de début, consultez Comment couper les espaces d'une variable Bash?
Håkon Hægland
24

Je me plagarise à partir d' une réponse précédente , pour le faire de manière robuste sans les risques de sécurité associés à eval:

expandPath() {
  local path
  local -a pathElements resultPathElements
  IFS=':' read -r -a pathElements <<<"$1"
  : "${pathElements[@]}"
  for path in "${pathElements[@]}"; do
    : "$path"
    case $path in
      "~+"/*)
        path=$PWD/${path#"~+/"}
        ;;
      "~-"/*)
        path=$OLDPWD/${path#"~-/"}
        ;;
      "~"/*)
        path=$HOME/${path#"~/"}
        ;;
      "~"*)
        username=${path%%/*}
        username=${username#"~"}
        IFS=: read -r _ _ _ _ _ homedir _ < <(getent passwd "$username")
        if [[ $path = */* ]]; then
          path=${homedir}/${path#*/}
        else
          path=$homedir
        fi
        ;;
    esac
    resultPathElements+=( "$path" )
  done
  local result
  printf -v result '%s:' "${resultPathElements[@]}"
  printf '%s\n' "${result%:}"
}

...utilisé comme...

path=$(expandPath '~/hello')

Alternativement, une approche plus simple qui utilise evalavec soin:

expandPath() {
  case $1 in
    ~[+-]*)
      local content content_q
      printf -v content_q '%q' "${1:2}"
      eval "content=${1:0:2}${content_q}"
      printf '%s\n' "$content"
      ;;
    ~*)
      local content content_q
      printf -v content_q '%q' "${1:1}"
      eval "content=~${content_q}"
      printf '%s\n' "$content"
      ;;
    *)
      printf '%s\n' "$1"
      ;;
  esac
}
Charles Duffy
la source
4
En regardant votre code, il semble que vous utilisez un canon pour tuer un moustique. Il y a obtenu d'une façon beaucoup plus simple ..
Gino
2
@Gino, il y a sûrement un moyen plus simple; la question est de savoir s'il existe un moyen plus simple qui soit également sécurisé.
Charles Duffy
2
@Gino, ... Je ne suppose que l' on peut utiliser printf %qpour échapper à tout , mais le tilde, et puis utiliser evalsans risque.
Charles Duffy
1
@Gino, ... et ainsi implémenté.
Charles Duffy
4
Sûr, oui, mais très incomplet. Mon code n'est pas complexe pour le plaisir - il est complexe car les opérations réelles effectuées par l'expansion du tilde sont complexes.
Charles Duffy
10

Un moyen sûr d'utiliser eval est "$(printf "~/%q" "$dangerous_path")". Notez que c'est spécifique à bash.

#!/bin/bash

relativepath=a/b/c
eval homedir="$(printf "~/%q" "$relativepath")"
echo $homedir # prints home path

Voir cette question pour plus de détails

Notez également que sous zsh, ce serait aussi simple que echo ${~dangerous_path}

Eddygeek
la source
echo ${~root}ne me donne pas de sortie sur zsh (mac os x)
Orwellophile
export test="~root/a b"; echo ${~test}
Gyscos
9

Que dis-tu de ça:

path=`realpath "$1"`

Ou:

path=`readlink -f "$1"`
Geai
la source
semble joli, mais realpath n'existe pas sur mon mac. Et vous devriez écrire path = $ (realpath "$ 1")
Hugo le
Salut @Hugo. Vous pouvez compiler votre propre realpathcommande en C. Par exemple, vous pouvez générer un exécutable en realpath.exeutilisant bash et gcc à partir de cette ligne de commande: gcc -o realpath.exe -x c - <<< $'#include <stdlib.h> \n int main(int c,char**v){char p[9999]; realpath(v[1],p); puts(p);}'. Cheers
olibre
@Quuxplusone n'est pas vrai, du moins sous Linux: realpath ~->/home/myhome
blueFast
iv'e l'a utilisé avec brew sur mac
nhed
1
@dangonfast Cela ne fonctionnera pas si vous définissez le tilde entre guillemets, le résultat est <workingdir>/~.
Murphy le
7

Expansion (sans jeu de mots) sur les réponses de birryree et halloleo: L'approche générale est d'utiliser eval, mais elle s'accompagne de quelques mises en garde importantes, à savoir les espaces et la redirection de sortie ( >) dans la variable. Ce qui suit semble fonctionner pour moi:

mypath="$1"

if [ -e "`eval echo ${mypath//>}`" ]; then
    echo "FOUND $mypath"
else
    echo "$mypath NOT FOUND"
fi

Essayez-le avec chacun des arguments suivants:

'~'
'~/existing_file'
'~/existing file with spaces'
'~/nonexistant_file'
'~/nonexistant file with spaces'
'~/string containing > redirection'
'~/string containing > redirection > again and >> again'

Explication

  • Le ${mypath//>}supprime les >caractères qui pourraient écraser un fichier pendant le eval.
  • C'est eval echo ...ce que fait l'expansion réelle du tilde
  • Les guillemets doubles autour de l' -eargument sont pour la prise en charge des noms de fichiers avec des espaces.

Il existe peut-être une solution plus élégante, mais c'est ce que j'ai pu proposer.

Noach Magedman
la source
3
Vous pourriez envisager de regarder le comportement avec des noms contenant $(rm -rf .).
Charles Duffy
1
Cela ne coupe-t-il pas les chemins contenant réellement des >caractères?
Radon Rosborough
2

Je crois que c'est ce que tu cherches

magic() { # returns unexpanded tilde express on invalid user
    local _safe_path; printf -v _safe_path "%q" "$1"
    eval "ln -sf ${_safe_path#\\} /tmp/realpath.$$"
    readlink /tmp/realpath.$$
    rm -f /tmp/realpath.$$
}

Exemple d'utilisation:

$ magic ~nobody/would/look/here
/var/empty/would/look/here

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand
Orwellophile
la source
Je suis surpris que cela printf %q n'échappe pas aux principaux tildes - il est presque tentant de signaler cela comme un bogue, car c'est une situation dans laquelle il échoue à son objectif déclaré. Cependant, en attendant, un bon appel!
Charles Duffy
1
En fait - ce bogue est corrigé entre 3.2.57 et 4.3.18, donc ce code ne fonctionne plus.
Charles Duffy
1
Bon point, je me suis ajusté au code pour supprimer le début \ s'il existe, donc tout a été corrigé et a fonctionné :) Je testais sans citer les arguments, donc il était en expansion avant d'appeler la fonction.
Orwellophile
1

Voici ma solution:

#!/bin/bash


expandTilde()
{
    local tilde_re='^(~[A-Za-z0-9_.-]*)(.*)'
    local path="$*"
    local pathSuffix=

    if [[ $path =~ $tilde_re ]]
    then
        # only use eval on the ~username portion !
        path=$(eval echo ${BASH_REMATCH[1]})
        pathSuffix=${BASH_REMATCH[2]}
    fi

    echo "${path}${pathSuffix}"
}



result=$(expandTilde "$1")

echo "Result = $result"
Gino
la source
De plus, compter sur echosignifie que cela expandTilde -nne se comportera pas comme prévu, et le comportement avec des noms de fichiers contenant des barres obliques inverses n'est pas défini par POSIX. Voir pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy
Bonne prise. J'utilise normalement une machine mono-utilisateur, donc je n'ai pas pensé à gérer ce cas. Mais je pense que la fonction pourrait facilement être améliorée pour gérer cet autre cas en passant par le fichier / etc / passwd pour l'autre utilisateur. Je vais le laisser comme un exercice pour quelqu'un d'autre :).
Gino
J'ai déjà fait cet exercice (et traité le cas OLDPWD et d'autres) dans une réponse que vous avez jugée trop complexe. :)
Charles Duffy
en fait, je viens de trouver une solution en une ligne assez simple qui devrait gérer le cas de l'autre utilisateur: path = $ (eval echo $ orgPath)
Gino
1
FYI: Je viens de mettre à jour ma solution afin qu'elle puisse désormais gérer correctement ~ nom d'utilisateur. Et cela devrait également être assez sûr. Même si vous mettez un '/ tmp / $ (rm -rf / *)' comme argument, il devrait le gérer correctement.
Gino
1

Utilisez simplement evalcorrectement: avec validation.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
mikeserv
la source
C'est probablement sûr - je n'ai pas trouvé de cas pour lequel il échoue. Cela dit, si nous parlons d'utiliser eval "correctement", je dirais que la réponse d'Orwellophile suit la meilleure pratique: je fais confiance au shell printf %qpour échapper aux choses en toute sécurité plus que je ne fais confiance au code de validation écrit à la main pour ne pas avoir de bogues .
Charles Duffy
@Charles Duffy - c'est idiot. le shell peut ne pas avoir de% q - et printfest une $PATHcommande 'd.
mikeserv
1
Cette question n'est-elle pas taguée bash? Si tel printfest le cas, est une fonction intégrée et %qest garantie d'être présente.
Charles Duffy
@Charles Duffy - quelle version?
mikeserv
1
@Charles Duffy - c'est ... assez tôt. mais je pense toujours que c'est bizarre que vous fassiez confiance à un argument% q plus que vous ne coderiez juste devant vos yeux, j'en ai bashassez utilisé avant de savoir ne pas lui faire confiance. essayer:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv
1

Voici la fonction POSIX équivalente à la réponse Bash de Håkon Hægland

expand_tilde() {
    tilde_less="${1#\~/}"
    [ "$1" != "$tilde_less" ] && tilde_less="$HOME/$tilde_less"
    printf '%s' "$tilde_less"
}

10/12/2017 edit: ajoutez '%s'par @CharlesDuffy dans les commentaires.

go2null
la source
1
printf '%s\n' "$tilde_less", peut-être? Sinon, il se comportera mal si le nom de fichier en cours de développement contient des barres obliques inverses %s, ou une autre syntaxe significative pour printf. En dehors de cela, cependant, c'est une excellente réponse - correcte (quand les extensions bash / ksh n'ont pas besoin d'être couvertes), évidemment sûre (pas de bavardage avec eval) et laconique.
Charles Duffy
1

pourquoi ne pas se plonger directement dans l'obtention du répertoire personnel de l'utilisateur avec getent?

$ getent passwd mike | cut -d: -f6
/users/mike
Paul M
la source
0

Juste pour étendre la réponse de birryree pour les chemins avec des espaces: vous ne pouvez pas utiliser la evalcommande telle quelle car elle sépare l'évaluation par des espaces. Une solution consiste à remplacer temporairement les espaces pour la commande eval:

mypath="~/a/b/c/Something With Spaces"
expandedpath=${mypath// /_spc_}    # replace spaces 
eval expandedpath=${expandedpath}  # put spaces back
expandedpath=${expandedpath//_spc_/ }
echo "$expandedpath"    # prints e.g. /Users/fred/a/b/c/Something With Spaces"
ls -lt "$expandedpath"  # outputs dir content

Cet exemple repose bien sûr sur l'hypothèse de mypathne jamais contenir la séquence char "_spc_".

halloleo
la source
1
Ne fonctionne pas avec les onglets, les retours à la ligne ou quoi que ce soit d'autre dans IFS ... et ne fournit pas de sécurité autour des métacaractères comme les chemins contenant$(rm -rf .)
Charles Duffy
0

Vous trouverez peut-être cela plus facile à faire en python.

(1) Depuis la ligne de commande unix:

python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' ~/fred

Résulte en:

/Users/someone/fred

(2) Dans un script bash en une seule fois - enregistrez-le sous test.sh:

#!/usr/bin/env bash

thepath=$(python -c 'import os; import sys; print os.path.expanduser(sys.argv[1])' $1)

echo $thepath

L'exécution des bash ./test.shrésultats en:

/Users/someone/fred

(3) En tant qu'utilitaire - enregistrez-le expanduserquelque part sur votre chemin, avec les autorisations d'exécution:

#!/usr/bin/env python

import sys
import os

print os.path.expanduser(sys.argv[1])

Cela pourrait ensuite être utilisé sur la ligne de commande:

expanduser ~/fred

Ou dans un script:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath
Chris Johnson
la source
Ou que diriez-vous de ne passer que '~' à Python, en retournant "/ home / fred"?
Tom Russell
1
Besoin de citations moar. echo $thepathest buggy; doit être echo "$thepath"de corriger les cas moins rares (les noms avec des tabulations ou des séries d'espaces étant convertis en espaces simples; les noms avec des globs les ayant développés), ou printf '%s\n' "$thepath"de corriger les plus rares aussi (c'est-à-dire un fichier nommé -n, ou un fichier avec les littéraux anti-slash sur un système compatible XSI). De même,thepath=$(expanduser "$1")
Charles Duffy
... pour comprendre ce que je voulais dire à propos des littéraux anti - slash, voir pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - POSIX permet echode se comporter d'une manière complètement définie par l'implémentation si un argument contient des anti-slash; les extensions XSI facultatives de POSIX imposent des comportements d'expansion par défaut (pas -eou -Enécessaires) pour ces noms.
Charles Duffy
0

Le plus simple : remplacez «magic» par «eval echo».

$ eval echo "~"
/whatever/the/f/the/home/directory/is

Problème: vous allez rencontrer des problèmes avec d'autres variables car eval est mauvais. Par exemple:

$ # home is /Users/Hacker$(s)
$ s="echo SCARY COMMAND"
$ eval echo $(eval echo "~")
/Users/HackerSCARY COMMAND

Notez que le problème de l'injection ne se produit pas lors de la première expansion. Donc, si vous deviez simplement remplacer magicpar eval echo, vous devriez être d'accord. Mais si vous le faites echo $(eval echo ~), ce serait susceptible d'être injecté.

De même, si vous le faites à la eval echo ~place eval echo "~", cela compterait comme deux fois expansé et l'injection serait donc possible immédiatement.

Karim Alibhai
la source
1
Contrairement à ce que vous avez dit, ce code n'est pas sûr . Par exemple, test s='echo; EVIL_COMMAND'. (Il échouera car il EVIL_COMMANDn'existe pas sur votre ordinateur. Mais si cette commande avait été rm -r ~par exemple, elle aurait supprimé votre répertoire personnel.)
Konrad Rudolph
0

J'ai fait cela avec une substitution de paramètre variable après la lecture du chemin en utilisant read -e (entre autres). Ainsi, l'utilisateur peut compléter le chemin par tabulation, et si l'utilisateur entre un chemin ~, il est trié.

read -rep "Enter a path:  " -i "${testpath}" testpath 
testpath="${testpath/#~/${HOME}}" 
ls -al "${testpath}" 

L'avantage supplémentaire est que s'il n'y a pas de tilde, rien n'arrive à la variable, et s'il y a un tilde mais pas en première position, il est également ignoré.

(J'inclus le -i pour la lecture puisque j'utilise ceci dans une boucle afin que l'utilisateur puisse réparer le chemin s'il y a un problème.)

JamesIsIn
la source