Come espandere manualmente una variabile speciale (es: ~ tilde) in bash


135

Ho una variabile nel mio script bash il cui valore è qualcosa del genere:

~/a/b/c

Si noti che è una tilde non espansa. Quando faccio ls -lt su questa variabile (lo chiamo $ VAR), non ottengo tale directory. Voglio lasciare bash interpretare / espandere questa variabile senza eseguirla. In altre parole, voglio che bash esegua eval ma non esegua il comando valutato. È possibile in bash?

Come sono riuscito a passare questo nel mio script senza espansione? Ho superato l'argomento circondandolo con doppie virgolette.

Prova questo comando per vedere cosa intendo:

ls -lt "~"

Questa è esattamente la situazione in cui mi trovo. Voglio che la tilde si espanda. In altre parole, con cosa dovrei sostituire la magia per rendere identici questi due comandi:

ls -lt ~/abc/def/ghi

e

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

Si noti che ~ / abc / def / ghi potrebbe esistere o meno.


4
Potresti trovare utile l' espansione di Tilde tra virgolette . Per lo più, ma non del tutto, evita l'utilizzo eval.
Jonathan Leffler,

2
In che modo la tua variabile è stata assegnata con una tilde non espansa? Forse tutto ciò che serve è assegnare quella variabile con le tilde al di fuori delle virgolette. foo=~/"$filepath"oppurefoo="$HOME/$filepath"
Chad Skeeters,

dir="$(readlink -f "$dir")"
Jack Wasey,

Risposte:


98

A causa della natura di StackOverflow, non posso semplicemente rendere questa risposta non accettata, ma negli ultimi 5 anni da quando l'ho pubblicata ci sono state risposte di gran lunga migliori della mia risposta dichiaratamente rudimentale e piuttosto cattiva (ero giovane, non uccidere me).

Le altre soluzioni in questo thread sono soluzioni più sicure e migliori. Preferibilmente, andrei con uno di questi due:


Risposta originale per scopi storici (ma per favore non usare questo)

Se non sbaglio, "~"non verrà espanso da uno script bash in quel modo perché viene trattato come una stringa letterale "~". Puoi forzare l'espansione in evalquesto modo.

#!/bin/bash

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

In alternativa, utilizzare solo ${HOME}se si desidera la home directory dell'utente.


3
Hai una correzione per quando la variabile contiene uno spazio?
Hugo,

34
Ho trovato ${HOME}molto attraente. C'è qualche motivo per non rendere questa la tua raccomandazione principale? In ogni caso, grazie!
salvia,

1
+1 - Avevo bisogno di espandere ~ $ some_other_user ed eval funziona bene quando $ HOME non funzionerà perché non ho bisogno della home dell'utente corrente.
olivecoder

11
L'uso evalè un suggerimento orribile, è davvero un male che ottenga così tanti voti positivi. Incontrerai tutti i tipi di problemi quando il valore della variabile contiene meta caratteri shell.
user2719058

1
Non ho potuto finire il mio commento in quel momento e, quindi, non mi è stato permesso di modificarlo in seguito. Quindi sono grato (grazie ancora @birryree) per questa soluzione che mi ha aiutato nel mio contesto specifico in quel momento. Grazie Charles per avermi fatto conoscere.
olivecoder,

114

Se la variabile varviene inserita dall'utente, noneval deve essere utilizzata per espandere la tilde mediante

eval var=$var  # Do not use this!

Il motivo è: l'utente potrebbe accidentalmente (o di proposito) digitare ad esempio var="$(rm -rf $HOME/)"con possibili conseguenze disastrose.

Un modo migliore (e più sicuro) è usare l'espansione dei parametri di Bash:

var="${var/#\~/$HOME}"

8
Come potresti cambiare ~ userName / invece che solo ~ /?
aspergillusOryzae,

3
Qual è lo scopo di #in "${var/#\~/$HOME}"?
Jahid,

3
@Jahid È spiegato nel manuale . Forza la tilde ad abbinarsi solo all'inizio di $var.
Håkon Hægland,

1
Grazie. (1) Perché abbiamo bisogno \~cioè di scappare ~? (2) La tua risposta presuppone che ~sia il primo carattere in $var. Come possiamo ignorare gli spazi bianchi principali in $var?
Tim

1
@Tim Grazie per il commento. Sì, hai ragione, non abbiamo bisogno di scappare da una tilde a meno che non sia il primo carattere di una stringa non quotata o non stia seguendo una :stringa non quotata. Maggiori informazioni nei documenti . Per rimuovere lo spazio bianco iniziale, vedere Come tagliare gli spazi bianchi da una variabile Bash?
Håkon Hægland,

24

Plagarando me stesso da una risposta precedente , per farlo in modo robusto senza i rischi per la sicurezza associati a 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%:}"
}

...usato come...

path=$(expandPath '~/hello')

In alternativa, un approccio più semplice che utilizza evalcon attenzione:

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
}

4
Guardando il tuo codice sembra che tu stia usando un cannone per uccidere una zanzara. C'è got a essere un modo molto più semplice ..
Gino

2
@Gino, c'è sicuramente un modo più semplice; la domanda è se esiste un modo più semplice che sia anche sicuro.
Charles Duffy,

2
@Gino, ... io non credo che si può usare printf %qper sfuggire tutto, ma la tilde, e poi usare evalsenza rischi.
Charles Duffy,

1
@Gino, ... e così implementato.
Charles Duffy,

3
Sicuro, sì, ma molto incompleto. Il mio codice non è complesso per il gusto di farlo, è complesso perché le operazioni effettive eseguite dall'espansione tilde sono complesse.
Charles Duffy,

10

Un modo sicuro per usare eval è "$(printf "~/%q" "$dangerous_path")". Nota che è specifico per bash.

#!/bin/bash

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

Vedi questa domanda per i dettagli

Inoltre, nota che sotto zsh questo sarebbe semplice come echo ${~dangerous_path}


echo ${~root}dammi nessun output su zsh (mac os x)
Orwellophile,

export test="~root/a b"; echo ${~test}
Gyscos,

9

Cosa ne pensi di questo:

path=`realpath "$1"`

O:

path=`readlink -f "$1"`

sembra carino, ma realpath non esiste sul mio mac. E dovresti scrivere path = $ (realpath "$ 1")
Hugo

Ciao @Hugo. È possibile compilare il proprio realpathcomando nel caso C. Per, è possibile generare un file eseguibile realpath.exeutilizzando bash e gcc da questa riga di comando: 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);}'. Saluti
olibre

@Quuxplusone non è vero, almeno su Linux: realpath ~->/home/myhome
blueFast

iv'e utilizzato con birra su Mac
nhed

1
@dangonfast Questo non funzionerà se si imposta la tilde tra virgolette, il risultato è <workingdir>/~.
Murphy,

7

Espandere (nessun gioco di parole previsto) sulle risposte di birryree e halloleo: l'approccio generale è usare eval, ma viene fornito con alcuni avvertimenti importanti, vale a dire gli spazi e il reindirizzamento dell'output ( >) nella variabile. Quanto segue sembra funzionare per me:

mypath="$1"

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

Provalo con ciascuno dei seguenti argomenti:

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

Spiegazione

  • Il ${mypath//>}strisce fuori >i caratteri che potrebbe massacravano un file durante il eval.
  • La eval echo ...è quello che esegue l'espansione tilde effettivo
  • Le doppie virgolette intorno -eall'argomento servono per supportare nomi di file con spazi.

Forse c'è una soluzione più elegante, ma è quello che sono riuscito a trovare.


3
Potresti considerare il comportamento con nomi contenenti $(rm -rf .).
Charles Duffy,

1
Questo non si interrompe su percorsi che in realtà contengono >caratteri?
Radon Rosborough,

2

Credo che questo sia quello che stai cercando

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.$$
}

Esempio di utilizzo:

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

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand

Sono sorpreso che printf %q non sfugga alle principali tilde: è quasi tentato di archiviarlo come un bug, in quanto è una situazione in cui fallisce al suo scopo dichiarato. Tuttavia, nel frattempo, una buona chiamata!
Charles Duffy,

1
In realtà - questo bug è stato corretto tra 3.2.57 e 4.3.18, quindi questo codice non funziona più.
Charles Duffy,

1
Bene, mi sono adattato al codice per rimuovere il comando \ se esiste, quindi tutto risolto e funzionante :) Stavo testando senza citare gli argomenti, quindi si stava espandendo prima di chiamare la funzione.
Orwellophile,

1

Ecco la mia soluzione:

#!/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"

Inoltre, fare affidamento su echosignifica che expandTilde -nnon si comporterà come previsto e il comportamento con nomi di file contenenti barre rovesciate non è definito da POSIX. Vedi pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy,

Buona pesca. Normalmente uso una macchina per un utente, quindi non ho pensato di gestire quel caso. Ma penso che la funzione potrebbe essere facilmente migliorata per gestire questo altro caso sfogliando il file / etc / passwd per l'altro utente. Lo lascerò come esercizio per qualcun altro :).
Gino,

Ho già fatto questo esercizio (e gestito il caso OLDPWD e altri) in una risposta che hai ritenuto troppo complessa. :)
Charles Duffy,

in realtà, ho appena trovato una soluzione a una riga abbastanza semplice che dovrebbe gestire il caso dell'altro utente: path = $ (eval echo $ orgPath)
Gino

1
Cordiali saluti: Ho appena aggiornato la mia soluzione in modo che ora possa gestire correttamente ~ username. E dovrebbe anche essere abbastanza sicuro. Anche se inserisci un '/ tmp / $ (rm -rf / *)' come argomento, dovrebbe gestirlo con grazia.
Gino,

1

Basta usare evalcorrettamente: con validazione.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"

Questo è probabilmente sicuro - non ho trovato un caso per cui fallisce. Detto questo, se parleremo usando eval "correttamente", direi che la risposta di Orwellophile segue la migliore pratica: confido che la shell printf %qsfugga alle cose in modo sicuro più di quanto mi fidi del codice di validazione scritto a mano per non avere bug .
Charles Duffy,

@Charles Duffy - è sciocco. la shell potrebbe non avere un% q - ed printfè un $PATHcomando 'd.
Mikeserv,

1
Questa domanda non è taggata bash? In tal caso, printfè un built-in ed %qè garantito per essere presente.
Charles Duffy,

@Charles Duffy - quale versione?
Mikeserv,

1
@Charles Duffy - è ... abbastanza presto. ma continuo a pensare che sia strano che ti fidi di un% q arg in più di quello che codificheresti davanti ai tuoi occhi, ne bashho usato abbastanza prima da sapere di non fidarmi. prova:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv il

1

Ecco l'equivalente della funzione POSIX della risposta Bash di Håkon Hægland

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

10-12-2017 modifica: aggiungi '%s'per @CharlesDuffy nei commenti.


1
printf '%s\n' "$tilde_less", Forse? Altrimenti si comporterà in modo errato se il nome file che viene espanso contiene barre rovesciate %so altre sintassi significative printf. Oltre a questo, però, questa è un'ottima risposta - corretta (quando non è necessario coprire le estensioni bash / ksh), ovviamente sicura (senza confusione eval) e concisa.
Charles Duffy,

1

perché non approfondire direttamente su come ottenere la home directory dell'utente con getent?

$ getent passwd mike | cut -d: -f6
/users/mike

0

Solo per estendere la risposta di birryree per i percorsi con spazi: non è possibile utilizzare il evalcomando così com'è perché separa la valutazione dagli spazi. Una soluzione è sostituire temporaneamente gli spazi per il comando 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

Questo esempio si basa ovviamente sul presupposto che mypathnon contiene mai la sequenza di caratteri "_spc_".


1
Non funziona con le schede, o le nuove righe o qualsiasi altra cosa in IFS ... e non fornisce sicurezza attorno ai metacaratteri come i percorsi contenenti$(rm -rf .)
Charles Duffy,

0

Potresti trovare questo più facile da fare in Python.

(1) Dalla riga di comando unix:

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

Risultati in:

/Users/someone/fred

(2) All'interno di uno script bash come una tantum - salvalo come test.sh:

#!/usr/bin/env bash

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

echo $thepath

Esecuzione bash ./test.shrisultati in:

/Users/someone/fred

(3) Come utility - salvalo come expanduserda qualche parte sul tuo percorso, con autorizzazioni di esecuzione:

#!/usr/bin/env python

import sys
import os

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

Questo potrebbe quindi essere utilizzato sulla riga di comando:

expanduser ~/fred

O in una sceneggiatura:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath

O che ne dici di passare solo '~' a Python, restituendo "/ home / fred"?
Tom Russell,

1
Ha bisogno di virgolette. echo $thepathè difettoso; deve essere echo "$thepath"quello di correggere i casi meno rari (nomi con tabulazioni o sequenze di spazi convertiti in spazi singoli; nomi con globs che li hanno espansi), o printf '%s\n' "$thepath"anche correggere quelli non comuni (cioè un file chiamato -n, o un file con valori letterali rovesciati su un sistema conforme a XSI). Allo stesso modo,thepath=$(expanduser "$1")
Charles Duffy il

... per capire cosa intendevo per valori letterali della barra rovesciata, vedere pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - POSIX consente echodi comportarsi in modo completamente definito dall'implementazione se qualsiasi argomento contiene barre rovesciate; le estensioni XSI opzionali ai comportamenti di espansione predefiniti del mandato POSIX (no -eo -Enecessari) per tali nomi.
Charles Duffy,

0

Più semplice : sostituisci "magia" con "eval echo".

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

Problema: incontrerai problemi con altre variabili perché eval è malvagio. Per esempio:

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

Si noti che il problema dell'iniezione non si verifica alla prima espansione. Quindi, se si dovesse sostituire semplicemente magiccon eval echo, si dovrebbe essere a posto. Ma se lo faiecho $(eval echo ~) , sarebbe suscettibile all'iniezione.

Allo stesso modo, se lo fai eval echo ~invece di eval echo "~"quello, verrebbe conteggiato come due volte espanso e quindi l'iniezione sarebbe possibile immediatamente.


1
Contrariamente a quanto hai detto, questo codice non è sicuro . Ad esempio, prova s='echo; EVIL_COMMAND'. (Fallirà perché EVIL_COMMANDnon esiste sul tuo computer. Ma se quel comando fosse stato rm -r ~per esempio, avrebbe cancellato la tua home directory.)
Konrad Rudolph

0

L'ho fatto con la sostituzione di parametri variabili dopo aver letto il percorso usando read -e (tra gli altri). In questo modo l'utente può completare il tab in modo completo e se l'utente immette un percorso ~ viene ordinato.

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

Il vantaggio aggiunto è che se non c'è tilde non succede nulla alla variabile, e se c'è una tilde ma non nella prima posizione, viene anche ignorato.

(Includo -i per la lettura poiché lo uso in un ciclo in modo che l'utente possa correggere il percorso in caso di problemi.)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.