So erweitern Sie eine spezielle Variable (z. B. ~ tilde) manuell in bash

135

Ich habe eine Variable in meinem Bash-Skript, deren Wert ungefähr so ​​lautet:

~/a/b/c

Beachten Sie, dass es sich um eine nicht erweiterte Tilde handelt. Wenn ich ls -lt für diese Variable mache (nenne es $ VAR), bekomme ich kein solches Verzeichnis. Ich möchte bash diese Variable interpretieren / erweitern lassen, ohne sie auszuführen. Mit anderen Worten, ich möchte, dass bash eval ausführt, aber den ausgewerteten Befehl nicht ausführt. Ist das in Bash möglich?

Wie habe ich es geschafft, dies ohne Erweiterung in mein Skript zu übertragen? Ich habe das Argument bestanden, indem ich es mit doppelten Anführungszeichen umgeben habe.

Versuchen Sie diesen Befehl, um zu sehen, was ich meine:

ls -lt "~"

Dies ist genau die Situation, in der ich mich befinde. Ich möchte, dass die Tilde erweitert wird. Mit anderen Worten, durch was soll ich Magie ersetzen, um diese beiden Befehle identisch zu machen:

ls -lt ~/abc/def/ghi

und

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

Beachten Sie, dass ~ / abc / def / ghi möglicherweise existiert oder nicht.

Madiyaan Damha
quelle
4
Möglicherweise ist die Tilde-Erweiterung auch in Anführungszeichen hilfreich. Meistens, aber nicht vollständig, wird die Verwendung vermieden eval.
Jonathan Leffler
2
Wie wurde Ihre Variable mit einer nicht erweiterten Tilde zugewiesen? Möglicherweise müssen Sie diese Variable nur mit der Tilde außerhalb der Anführungszeichen zuweisen. foo=~/"$filepath"oderfoo="$HOME/$filepath"
Chad Skeeters
dir="$(readlink -f "$dir")"
Jack Wasey

Antworten:

98

Aufgrund der Natur von StackOverflow kann ich diese Antwort nicht einfach nicht akzeptieren, aber in den letzten 5 Jahren, seit ich dies gepostet habe, gab es weitaus bessere Antworten als meine zugegebenermaßen rudimentäre und ziemlich schlechte Antwort (ich war jung, töte nicht mich).

Die anderen Lösungen in diesem Thread sind sicherere und bessere Lösungen. Am liebsten würde ich mit einem dieser beiden gehen:


Ursprüngliche Antwort für historische Zwecke (aber bitte nicht verwenden)

Wenn ich mich nicht irre, "~"wird es auf diese Weise nicht durch ein Bash-Skript erweitert, da es als Literalzeichenfolge behandelt wird "~". Sie können die Erweiterung auf evaldiese Weise erzwingen .

#!/bin/bash

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

Alternativ können Sie auch einfach verwenden, ${HOME}wenn Sie das Home-Verzeichnis des Benutzers möchten.

wkl
quelle
3
Haben Sie eine Lösung für den Fall, dass die Variable ein Leerzeichen enthält?
Hugo
34
${HOME}Am attraktivsten fand ich . Gibt es einen Grund, dies nicht zu Ihrer primären Empfehlung zu machen? Auf jeden Fall danke!
Salbei
1
+1 - Ich musste ~ $ some_other_user erweitern und eval funktioniert einwandfrei, wenn $ HOME nicht funktioniert, da ich den aktuellen Benutzer nicht zu Hause brauche.
Olivecoder
11
Verwenden evalist ein schrecklicher Vorschlag, es ist wirklich schlimm, dass es so viele positive Stimmen bekommt. Sie werden auf alle möglichen Probleme stoßen, wenn der Wert der Variablen Shell-Metazeichen enthält.
user2719058
1
Ich konnte meinen Kommentar zu diesem Zeitpunkt nicht beenden und durfte ihn später nicht mehr bearbeiten. Daher bin ich dankbar (nochmals vielen Dank an @birryree) für diese Lösung, da sie in meinem damaligen Kontext hilfreich war. Danke Charles, dass du mich darauf aufmerksam gemacht hast.
Olivecoder
114

Wenn die Variable varvom Benutzer eingegeben wird, evalsollte sie nicht zum Erweitern der Tilde mit verwendet werden

eval var=$var  # Do not use this!

Der Grund ist: Der Benutzer könnte versehentlich (oder absichtlich) tippen, zum Beispiel var="$(rm -rf $HOME/)"mit möglichen katastrophalen Folgen.

Eine bessere (und sicherere) Möglichkeit ist die Verwendung der Bash-Parametererweiterung:

var="${var/#\~/$HOME}"
Håkon Hægland
quelle
8
Wie könnten Sie ~ userName / anstatt nur ~ / ändern?
AspergillusOryzae
3
Was ist der Zweck von #in "${var/#\~/$HOME}"?
Jahid
3
@Jahid Es wird im Handbuch erklärt . Es zwingt die Tilde, erst am Anfang von übereinzustimmen $var.
Håkon Hægland
1
Vielen Dank. (1) Warum brauchen wir \~also Flucht ~? (2) Ihre Antwort geht davon aus, dass dies ~das erste Zeichen in ist $var. Wie können wir die führenden Leerzeichen in ignorieren $var?
Tim
1
@ Tim Danke für den Kommentar. Ja, Sie haben Recht, wir müssen einer Tilde nicht entkommen, es sei denn, es ist das erste Zeichen einer nicht zitierten Zeichenfolge oder es folgt einer :in einer nicht zitierten Zeichenfolge. Weitere Informationen in den Dokumenten . Informationen zum Entfernen führender Leerzeichen finden Sie unter Trimmen von Leerzeichen aus einer Bash-Variablen.
Håkon Hægland
24

Ich plagarisiere mich von einer vorherigen Antwort , um dies ohne die Sicherheitsrisiken zu tun, die mit Folgendem verbunden sind 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%:}"
}

...benutzt als...

path=$(expandPath '~/hello')

Alternativ ein einfacherer Ansatz, der evalsorgfältig verwendet:

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
quelle
4
Wenn Sie sich Ihren Code ansehen, sehen Sie aus, als würden Sie eine Mücke mit einer Kanone töten. Es gibt bekommt eine viel einfachere Art und Weise sein ..
Gino
2
@ Gino, es gibt sicherlich einen einfacheren Weg; Die Frage ist, ob es einen einfacheren Weg gibt, der auch sicher ist.
Charles Duffy
2
@Gino, ... ich kann annehmen , dass man verwenden kann printf %qalles zu entkommen , aber die Tilde, und dann verwenden , evalohne Risiko.
Charles Duffy
1
@Gino, ... und so umgesetzt.
Charles Duffy
2
Sicher, ja, aber sehr unvollständig. Mein Code ist zum Spaß nicht komplex - er ist komplex, weil die tatsächlichen Operationen, die durch die Tilde-Erweiterung ausgeführt werden, komplex sind.
Charles Duffy
10

Ein sicherer Weg, eval zu verwenden, ist "$(printf "~/%q" "$dangerous_path")". Beachten Sie, dass dies bash-spezifisch ist.

#!/bin/bash

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

Siehe diese Frage für Details

Beachten Sie auch, dass dies unter zsh so einfach wie wäre echo ${~dangerous_path}

Eddygeek
quelle
echo ${~root}gib mir keine Ausgabe auf zsh (mac os x)
Orwellophile
export test="~root/a b"; echo ${~test}
Gyscos
9

Wie wäre es damit:

path=`realpath "$1"`

Oder:

path=`readlink -f "$1"`
Jay
quelle
sieht gut aus, aber realpath existiert auf meinem Mac nicht. Und Sie müssten path = $ (realpath "$ 1") schreiben
Hugo
Hallo @Hugo. Sie können Ihren eigenen realpathBefehl in C kompilieren . Beispielsweise können Sie eine ausführbare Datei realpath.exemit bash und gcc über diese Befehlszeile generieren : 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);}'. Prost
olibre
@ Quuxplusone nicht wahr, zumindest unter Linux: realpath ~->/home/myhome
blueFast
iv'e verwendet es mit Gebräu auf dem Mac
nhed
1
@dangonfast Dies funktioniert nicht, wenn Sie die Tilde in Anführungszeichen setzen. Das Ergebnis ist <workingdir>/~.
Murphy
7

Erweitern (kein Wortspiel beabsichtigt) der Antworten von Birryree und Halloleo: Der allgemeine Ansatz ist die Verwendung eval, enthält jedoch einige wichtige Einschränkungen, nämlich Leerzeichen und Ausgabeumleitung ( >) in der Variablen. Folgendes scheint für mich zu funktionieren:

mypath="$1"

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

Versuchen Sie es mit jedem der folgenden Argumente:

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

Erläuterung

  • Die ${mypath//>}Streifen aus >Zeichen , die eine Datei während des clobber könnte eval.
  • Das eval echo ...ist es, was die eigentliche Tilde-Erweiterung bewirkt
  • Die doppelten Anführungszeichen um das -eArgument dienen zur Unterstützung von Dateinamen mit Leerzeichen.

Vielleicht gibt es eine elegantere Lösung, aber das konnte ich mir einfallen lassen.

Noach Magedman
quelle
3
Sie könnten das Verhalten mit Namen betrachten, die enthalten $(rm -rf .).
Charles Duffy
1
Bricht dies nicht auf Pfaden, die tatsächlich >Zeichen enthalten ?
Radon Rosborough
2

Ich glaube, das ist es, wonach du suchst

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

Anwendungsbeispiel:

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

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand
Orwellophil
quelle
Ich bin überrascht, dass printf %q es führenden Tildes nicht entgeht - es ist fast verlockend, dies als Fehler einzureichen, da es eine Situation ist, in der es an seinem angegebenen Zweck versagt. In der Zwischenzeit jedoch ein guter Anruf!
Charles Duffy
1
Eigentlich - dieser Fehler wurde irgendwann zwischen 3.2.57 und 4.3.18 behoben, sodass dieser Code nicht mehr funktioniert.
Charles Duffy
1
Guter Punkt, ich habe mich an den Code angepasst, um das führende \ zu entfernen, falls vorhanden, also alles behoben und funktioniert :) Ich habe getestet, ohne die Argumente zu zitieren, also wurde es vor dem Aufrufen der Funktion erweitert.
Orwellophile
1

Hier ist meine Lösung:

#!/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
quelle
Wenn Sie sich auf echoMittel verlassen, die expandTilde -nsich nicht wie erwartet verhalten, und das Verhalten mit Dateinamen, die Backslashes enthalten, ist von POSIX nicht definiert. Siehe pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy
Guter Fang. Normalerweise benutze ich einen Einbenutzer-Computer, daher habe ich nicht daran gedacht, diesen Fall zu behandeln. Ich denke jedoch, dass die Funktion leicht erweitert werden kann, um diesen anderen Fall zu behandeln, indem die Datei / etc / passwd für den anderen Benutzer durchsucht wird. Ich lasse es als Übung für jemand anderen :).
Gino
Ich habe diese Übung bereits in einer Antwort durchgeführt (und den OLDPWD-Fall und andere behandelt), die Sie als zu komplex erachteten. :)
Charles Duffy
Eigentlich habe ich gerade eine ziemlich einfache einzeilige Lösung gefunden, die den Fall des anderen Benutzers behandeln sollte: path = $ (eval echo $ orgPath)
Gino
1
Zu Ihrer Information: Ich habe gerade meine Lösung aktualisiert, damit sie jetzt ~ Benutzername korrekt verarbeiten kann. Und es sollte auch ziemlich sicher sein. Selbst wenn Sie ein '/ tmp / $ (rm -rf / *)' als Argument eingeben, sollte es ordnungsgemäß behandelt werden.
Gino
1

Einfach evalrichtig verwenden: mit Validierung.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
mikeserv
quelle
Dies ist wahrscheinlich sicher - ich habe keinen Fall gefunden, für den es fehlschlägt. Das heißt, wenn wir mit der Verwendung von eval "richtig" sprechen wollen, würde ich argumentieren, dass Orwellophiles Antwort der besseren Praxis folgt: Ich vertraue darauf, dass die Shell den printf %qDingen sicherer entkommt, als ich darauf vertraue, dass handgeschriebener Validierungscode keine Fehler enthält .
Charles Duffy
@ Charles Duffy - das ist albern. Die Shell hat möglicherweise kein% q - und printfist ein $PATH'd-Befehl.
Mikeserv
1
Ist diese Frage nicht markiert bash? Wenn ja, printfist ein eingebautes und %qist garantiert vorhanden.
Charles Duffy
@ Charles Duffy - welche Version?
Mikeserv
1
@ Charles Duffy - das ist ... ziemlich früh. Aber ich finde es immer noch seltsam, dass Sie einem% q arg mehr vertrauen würden, als Sie direkt vor Ihren Augen codieren würden. Ich habe bashgenug verwendet, um zu wissen, dass ich ihm nicht vertrauen soll. versuchen Sie:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv
1

Hier ist das POSIX-Funktionsäquivalent zu Håkon Hæglands Bash- Antwort

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

2017-12-10 bearbeiten: '%s'per @CharlesDuffy in den Kommentaren hinzufügen .

go2null
quelle
1
printf '%s\n' "$tilde_less", vielleicht? Andernfalls verhält es sich schlecht, wenn der zu erweiternde Dateiname Backslashes %soder eine andere Syntax enthält, die für printf. Davon abgesehen ist dies jedoch eine großartige Antwort - richtig (wenn Bash / Ksh-Erweiterungen nicht abgedeckt werden müssen), offensichtlich sicher (kein Mist eval) und knapp.
Charles Duffy
1

Warum nicht gleich mit getent in das Home-Verzeichnis des Benutzers eintauchen?

$ getent passwd mike | cut -d: -f6
/users/mike
Paul M.
quelle
0

Nur um die Antwort von birryree für Pfade mit Leerzeichen zu erweitern: Sie können den evalBefehl nicht unverändert verwenden , da er die Auswertung nach Leerzeichen trennt. Eine Lösung besteht darin, Leerzeichen für den Befehl eval vorübergehend zu ersetzen:

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

Dieses Beispiel basiert natürlich auf der Annahme, dass mypathdie Zeichenfolge niemals enthalten ist "_spc_".

Halloleo
quelle
1
Funktioniert nicht mit Registerkarten, Zeilenumbrüchen oder anderen Elementen in IFS ... und bietet keine Sicherheit für Metazeichen wie Pfade mit$(rm -rf .)
Charles Duffy
0

Möglicherweise ist dies in Python einfacher.

(1) Über die Unix-Befehlszeile:

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

Ergebnisse in:

/Users/someone/fred

(2) Innerhalb eines Bash-Skripts als einmalig - speichern Sie dies als test.sh:

#!/usr/bin/env bash

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

echo $thepath

Laufende bash ./test.shErgebnisse in:

/Users/someone/fred

(3) Als Dienstprogramm - speichern Sie dies als expanduserirgendwo auf Ihrem Pfad mit Ausführungsberechtigungen:

#!/usr/bin/env python

import sys
import os

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

Dies könnte dann in der Kommandozeile verwendet werden:

expanduser ~/fred

Oder in einem Skript:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath
Chris Johnson
quelle
Oder wie wäre es, wenn Sie nur '~' an Python übergeben und "/ home / fred" zurückgeben?
Tom Russell
1
Benötigt Moar Zitate. echo $thepathist fehlerhaft; muss sein echo "$thepath", um die weniger seltenen Fälle zu beheben (Namen mit Tabulatoren oder Leerzeichen, die in einzelne Leerzeichen konvertiert werden; Namen mit Globs, die sie erweitert haben), oder printf '%s\n' "$thepath"um auch die ungewöhnlichen Fälle zu beheben (dh eine Datei mit dem Namen -noder eine Datei mit Backslash-Literale auf einem XSI-kompatiblen System). Ebensothepath=$(expanduser "$1")
Charles Duffy
... um zu verstehen, was ich mit Backslash-Literalen gemeint habe, siehe pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - POSIX ermöglicht echoein vollständig implementierungsdefiniertes Verhalten, wenn ein Argument Backslashes enthält. Die optionalen XSI-Erweiterungen für POSIX schreiben standardmäßige (keine -eoder -Eerforderliche) Erweiterungsverhalten für solche Namen vor.
Charles Duffy
0

Am einfachsten : Ersetzen Sie "Magie" durch "Eval Echo".

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

Problem: Sie werden auf Probleme mit anderen Variablen stoßen, weil die Bewertung böse ist. Zum Beispiel:

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

Beachten Sie, dass das Problem der Injektion bei der ersten Erweiterung nicht auftritt. Also , wenn Sie sind einfach ersetzen magicmit eval echo, sollten Sie in Ordnung sein. Wenn Sie dies jedoch tun echo $(eval echo ~), ist dies anfällig für Injektionen.

Wenn Sie dies eval echo ~stattdessen tun eval echo "~", würde dies als doppelt erweitert gelten und daher wäre eine Injektion sofort möglich.

Karim Alibhai
quelle
1
Im Gegensatz zu dem, was Sie gesagt haben, ist dieser Code unsicher . Zum Beispiel testen s='echo; EVIL_COMMAND'. (Es wird fehlschlagen, weil EVIL_COMMANDes auf Ihrem Computer nicht vorhanden ist. Aber wenn dieser Befehl rm -r ~zum Beispiel gewesen wäre, hätte er Ihr Home-Verzeichnis gelöscht.)
Konrad Rudolph
0

Ich habe dies mit variabler Parameterersetzung getan, nachdem ich den Pfad unter anderem mit read -e eingelesen habe. So kann der Benutzer den Pfad auf der Registerkarte vervollständigen, und wenn der Benutzer einen ~ -Pfad eingibt, wird dieser sortiert.

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

Der zusätzliche Vorteil besteht darin, dass mit der Variablen nichts passiert, wenn keine Tilde vorhanden ist. Wenn eine Tilde vorhanden ist, sich jedoch nicht an der ersten Position befindet, wird sie ebenfalls ignoriert.

(Ich füge das -i zum Lesen ein, da ich es in einer Schleife verwende, damit der Benutzer den Pfad korrigieren kann, wenn ein Problem auftritt.)

JamesIsIn
quelle