Jak ręcznie rozwinąć specjalną zmienną (np. ~ Tyldę) w bash


145

Mam zmienną w moim skrypcie bash, której wartość jest mniej więcej taka:

~/a/b/c

Zwróć uwagę, że jest to nierozwinięta tylda. Kiedy robię ls -lt na tej zmiennej (nazwij ją $ ​​VAR), nie otrzymuję takiego katalogu. Chcę pozwolić bashowi zinterpretować / rozszerzyć tę zmienną bez jej wykonywania. Innymi słowy, chcę, aby bash uruchamiał eval, ale nie uruchamiał ocenianego polecenia. Czy to możliwe w bash?

Jak udało mi się przekazać to do mojego skryptu bez rozszerzenia? Przekazałem argument, otaczając go podwójnymi cudzysłowami.

Wypróbuj to polecenie, aby zobaczyć, co mam na myśli:

ls -lt "~"

Dokładnie w takiej sytuacji jestem. Chcę, aby tylda została rozszerzona. Innymi słowy, czym powinienem zastąpić magię, aby te dwa polecenia były identyczne:

ls -lt ~/abc/def/ghi

i

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

Zauważ, że ~ / abc / def / ghi może istnieć lub nie.


4
Może okazać się, że pomocne może okazać się rozszerzenie Tilde w cudzysłowie . Przeważnie, ale nie całkowicie, unika używania eval.
Jonathan Leffler,

2
W jaki sposób zmiennej przypisano nierozszerzoną tyldę? Może wszystko, czego potrzeba, to przypisanie tej zmiennej tyldy umieszczonej w cudzysłowie. foo=~/"$filepath"lubfoo="$HOME/$filepath"
Chad Skeeters

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

Odpowiedzi:


104

Ze względu na naturę StackOverflow nie mogę po prostu uczynić tej odpowiedzi nieakceptowaną, ale w ciągu ostatnich 5 lat, odkąd to opublikowałem, pojawiły się o wiele lepsze odpowiedzi niż moja, co prawda, szczątkowa i całkiem zła odpowiedź (byłem młody, nie zabijaj mnie).

Pozostałe rozwiązania w tym wątku są bezpieczniejsze i lepsze. Najlepiej wybrałbym jedną z tych dwóch:


Oryginalna odpowiedź do celów historycznych (ale nie używaj jej)

Jeśli się nie mylę, "~"nie zostanie w ten sposób rozwinięty przez skrypt bash, ponieważ jest traktowany jako dosłowny ciąg "~". W ten sposób możesz wymusić ekspansję eval.

#!/bin/bash

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

Alternatywnie, po prostu użyj, ${HOME}jeśli chcesz katalog domowy użytkownika.


3
Czy masz rozwiązanie, gdy zmienna ma w sobie spację?
Hugo,

34
Wydawało mi się ${HOME}najbardziej atrakcyjne. Czy jest jakiś powód, aby nie traktować tego jako głównej rekomendacji? W każdym razie dzięki!
sage

1
+1 - Musiałem rozszerzyć ~ $ some_other_user i eval działa dobrze, gdy $ HOME nie będzie działać, ponieważ nie potrzebuję bieżącego użytkownika domowego.
olivecoder

13
Używanie evaljest okropną sugestią, naprawdę źle, że zbiera tak wiele pozytywnych głosów. Gdy wartość zmiennej zawiera metaznaki powłoki, napotkasz różnego rodzaju problemy.
user2719058

1
Nie mogłem wtedy dokończyć komentarza, a potem nie pozwolono mi go później edytować. Jestem więc wdzięczny (jeszcze raz dziękuję @birryree) za to rozwiązanie, ponieważ pomogło w moim konkretnym kontekście w tamtym czasie. Dziękuję Charles za uświadomienie mi.
olivecoder

119

Jeśli zmienna varjest wprowadzany przez użytkownika, evalpowinien nie być stosowany w celu rozszerzenia za pomocą tyldy

eval var=$var  # Do not use this!

Powód jest taki: użytkownik może przez przypadek (lub celowo) typ, na przykład var="$(rm -rf $HOME/)"z możliwymi katastrofalnymi konsekwencjami.

Lepszym (i bezpieczniejszym) sposobem jest użycie rozszerzenia parametrów Bash:

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

8
Jak możesz zmienić ~ userName / zamiast tylko ~ /?
aspergillusOryzae

3
Jaki jest cel #w "${var/#\~/$HOME}"?
Jahid

4
@Jahid Wyjaśniono to w instrukcji . Zmusza tyldę do dopasowania tylko na początku $var.
Håkon Hægland

1
Dzięki. (1) Dlaczego potrzebujemy \~np. Ucieczki ~? (2) Twoja odpowiedź zakłada, że ~jest to pierwszy znak w $var. Jak możemy zignorować wiodące białe spacje $var?
Tim

2
Zmień w odpowiedzi wyjaśnienie, że „$ {var / # ...}” to specjalna składnia basha, która pasuje tylko na początku. Z adresem URL. Używałem basha od dziesięcioleci, ale tego nie znałem. Ponadto potencjalnym problemem z eval () jest wstrzyknięcie złośliwego kodu.
smci

25

Plagarowanie się na podstawie wcześniejszej odpowiedzi , aby zrobić to solidnie bez zagrożeń bezpieczeństwa związanych z 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%:}"
}

...użyty jako...

path=$(expandPath '~/hello')

Alternatywnie, prostsze podejście, które wykorzystuje evalostrożnie:

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
Patrząc na twój kod, wygląda na to, że używasz armaty do zabicia komara. Tam dostał się znacznie prostszy sposób ..
Gino

2
@Gino, z pewnością istnieje prostszy sposób; pytanie brzmi, czy istnieje prostszy sposób, który jest również bezpieczny.
Charles Duffy

2
@Gino ... Ja nie przypuszczam, że można użyć printf %qdo ucieczki wszystko oprócz tyldy, a następnie używać evalbez ryzyka.
Charles Duffy

1
@Gino, ... i tak zaimplementowane.
Charles Duffy

4
Bezpieczne, tak, ale bardzo niekompletne. Mój kod nie jest złożony dla samej przyjemności - jest złożony, ponieważ rzeczywiste operacje wykonywane przez rozwinięcie tyldy są złożone.
Charles Duffy

10

Bezpiecznym sposobem korzystania z eval jest "$(printf "~/%q" "$dangerous_path")". Zauważ, że jest to specyficzne dla basha.

#!/bin/bash

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

Zobacz to pytanie, aby uzyskać szczegółowe informacje

Zwróć też uwagę, że w przypadku zsh byłoby to tak proste, jak echo ${~dangerous_path}


echo ${~root}daj mi brak wyjścia na zsh (mac os x)
Orwellophile

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

9

Co powiesz na to:

path=`realpath "$1"`

Lub:

path=`readlink -f "$1"`

wygląda ładnie, ale realpath nie istnieje na moim Macu. I musiałbyś napisać path = $ (realpath "$ 1")
Hugo,

Cześć @Hugo. Można opracować własne realpathpolecenia w C. Dla przykładu, można wygenerować plik wykonywalny realpath.exeza pomocą bash i gcc z tej linii komend: 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);}'. Pozdrawiam
olibre

@Quuxplusone nie jest prawdziwe, przynajmniej na Linuksie: realpath ~->/home/myhome
blueFast

iv'e użyłem go do naparu w mac
nhed

1
@dangonfast To nie zadziała, jeśli ustawisz tyldę w cudzysłowach. Wynik jest taki <workingdir>/~.
Murphy

7

Rozszerzanie (gra słów nie jest zamierzona) na odpowiedziach birryree i halloleo: Ogólne podejście polega na użyciu eval, ale wiąże się z kilkoma ważnymi zastrzeżeniami, a mianowicie spacjami i przekierowaniem wyjścia ( >) w zmiennej. Wydaje mi się, że działa dla mnie:

mypath="$1"

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

Spróbuj z każdym z następujących argumentów:

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

Wyjaśnienie

  • ${mypath//>}Wycina >znaków, które mogłyby sprać plik czasie eval.
  • To eval echo ...jest to, co robi faktyczna ekspansja tyldy
  • Podwójne cudzysłowy wokół -eargumentu służą do obsługi nazw plików ze spacjami.

Być może jest bardziej eleganckie rozwiązanie, ale to właśnie udało mi się wymyślić.


3
Możesz rozważyć zachowanie z nazwami zawierającymi $(rm -rf .).
Charles Duffy,

1
Czy to jednak nie działa na ścieżkach, które faktycznie zawierają >znaki?
Radon Rosborough

2

Myślę, że właśnie tego szukasz

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

Przykładowe użycie:

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

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

Dziwię się, że printf %q nie wymyka się wiodącym tyldom - prawie kuszące jest zgłoszenie tego jako błędu, ponieważ jest to sytuacja, w której zawodzi w określonym celu. Jednak w międzyczasie dobry telefon!
Charles Duffy

1
Właściwie - ten błąd został naprawiony w pewnym momencie między 3.2.57 a 4.3.18, więc ten kod już nie działa.
Charles Duffy

1
Słuszna uwaga, dostosowałem kod tak, aby usunąć początkowy \ jeśli istnieje, więc wszystko naprawione i działało :) Testowałem bez cytowania argumentów, więc rozszerzał się przed wywołaniem funkcji.
Orwellophile

1

Oto moje rozwiązanie:

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

Ponadto poleganie na echooznacza, że expandTilde -nnie będzie zachowywał się zgodnie z oczekiwaniami, a zachowanie z nazwami plików zawierającymi odwrotne ukośniki jest niezdefiniowane przez POSIX. Zobacz pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy

Dobry chwyt. Zwykle używam komputera dla jednego użytkownika, więc nie pomyślałem, żeby zająć się tym przypadkiem. Ale myślę, że tę funkcję można łatwo ulepszyć, aby obsłużyć ten inny przypadek, przechodząc przez plik / etc / passwd dla innego użytkownika. Zostawię to jako ćwiczenie dla kogoś innego :).
Gino

Wykonałem już to ćwiczenie (i zająłem się sprawą OLDPWD i innymi) w odpowiedzi, którą uważasz za zbyt złożoną. :)
Charles Duffy

Właściwie, to po prostu znaleźć dość proste rozwiązanie jednego wiersza, który powinien obsługiwać ten otheruser sprawy: path = $ (eval echo $ orgPath)
Gino

1
FYI: Właśnie zaktualizowałem moje rozwiązanie, aby teraz poprawnie obsługiwało ~ nazwa użytkownika. Powinien też być dość bezpieczny. Nawet jeśli wstawisz „/ tmp / $ (rm -rf / *)” jako argument, powinien on obsłużyć go wdzięcznie.
Gino

1

Po prostu używaj evalpoprawnie: z walidacją.

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

Jest to prawdopodobnie bezpieczne - nie znalazłem przypadku, w którym się nie udaje. To powiedziawszy, jeśli zamierzamy mówić o używaniu eval „poprawnie”, argumentowałbym, że odpowiedź Orwellophile jest zgodna z lepszą praktyką: bardziej ufam powłoce, printf %qaby uciec od rzeczy bezpiecznie, niż ufam odręcznemu kodowi walidacyjnemu, aby nie zawierał błędów .
Charles Duffy

@Charles Duffy - to głupie. powłoka może nie mieć% q - i printfjest $PATHpoleceniem 'd.
mikeserv

1
Czy to pytanie nie jest oznaczone tagiem bash? Jeśli tak, printfjest elementem wbudowanym i %qna pewno będzie obecny.
Charles Duffy

@Charles Duffy - jaka wersja?
mikeserv

1
@Charles Duffy - to ... dość wcześnie. ale nadal uważam, że to dziwne, że ufasz argumentowi% q bardziej niż kodowałbyś na twoich oczach, użyłem go bashwystarczająco dużo wcześniej, by wiedzieć, że mu nie ufam. spróbuj:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv

1

Najprostsze : zamień „magię” na „eval echo”.

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

Problem: Będziesz mieć problemy z innymi zmiennymi, ponieważ eval jest zły. Na przykład:

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

Zauważ, że problem z wtryskiem nie występuje przy pierwszym rozszerzeniu. Więc jeśli były po prostu zastąpić magicz eval echo, powinno być w porządku. Ale jeśli to zrobisz echo $(eval echo ~), będzie to podatne na zastrzyk.

Podobnie, jeśli to zrobisz eval echo ~zamiast tego eval echo "~", liczy się to jako dwukrotne rozwinięcie, a zatem wstrzyknięcie będzie możliwe od razu.


1
Wbrew temu, co powiedziałeś, ten kod jest niebezpieczny . Na przykład test s='echo; EVIL_COMMAND'. (Nie powiedzie się, ponieważ EVIL_COMMANDnie istnieje na twoim komputerze. Ale gdyby to polecenie było rm -r ~na przykład, spowodowałoby usunięcie twojego katalogu domowego.)
Konrad Rudolph

1

Oto funkcja równoważna z POSIX Hakon Hægland za bash odpowiedź

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

2017-12-10 edycja: dodaj '%s'w komentarzach @CharlesDuffy.


1
printf '%s\n' "$tilde_less", być może? W przeciwnym razie będzie źle działać, jeśli rozwijana nazwa pliku zawiera ukośniki odwrotne %slub inną składnię, która ma znaczenie dla printf. Poza tym jest to jednak świetna odpowiedź - poprawna (gdy rozszerzenia bash / ksh nie muszą być zakryte), oczywiście bezpieczna (bez manipulowania eval) i zwięzła.
Charles Duffy

1

dlaczego nie zagłębić się bezpośrednio w pobieranie katalogu domowego użytkownika za pomocą getent?

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

0

Wystarczy rozszerzyć odpowiedź birryree na ścieżki o spacje: Nie można użyć evalpolecenia w takiej postaci, w jakiej jest, ponieważ oddziela ocenę spacjami. Jednym z rozwiązań jest tymczasowe zastąpienie spacji dla polecenia 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

Ten przykład opiera się oczywiście na założeniu, że mypathnigdy nie zawiera sekwencji znaków "_spc_".


1
Nie działa z tabulatorami, znakami nowej linii ani z niczym innym w IFS ... i nie zapewnia bezpieczeństwa wokół metaznaków, takich jak ścieżki zawierające$(rm -rf .)
Charles Duffy

0

Może się to wydawać łatwiejsze w Pythonie.

(1) Z wiersza poleceń unixa:

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

Prowadzi do:

/Users/someone/fred

(2) W ramach skryptu bash jednorazowo - zapisz to jako test.sh:

#!/usr/bin/env bash

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

echo $thepath

Uruchamianie bash ./test.shwyników w:

/Users/someone/fred

(3) Jako narzędzie - zapisz to expandusergdzieś na swojej ścieżce, z uprawnieniami do wykonywania:

#!/usr/bin/env python

import sys
import os

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

Można to następnie użyć w wierszu poleceń:

expanduser ~/fred

Lub w scenariuszu:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath

A co powiesz na przekazanie tylko '~' do Pythona, zwracając „/ home / fred”?
Tom Russell

1
Potrzebuje cytatów moar. echo $thepathjest buggy; musi echo "$thepath"naprawiać mniej rzadkie przypadki (nazwy z tabulatorami lub ciągami spacji są konwertowane na pojedyncze spacje; nazwy z rozszerzeniami globów) lub printf '%s\n' "$thepath"też naprawiać rzadkie (np. plik o nazwie -nlub plik z literały odwrotnego ukośnika w systemie zgodnym z XSI). Podobniethepath=$(expanduser "$1")
Charles Duffy

... aby zrozumieć, co mam na myśli mówiąc o literałach odwrotnego ukośnika, zobacz pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - POSIX pozwala echona zachowanie w sposób całkowicie zdefiniowany przez implementację, jeśli którykolwiek argument zawiera ukośniki odwrotne; opcjonalne rozszerzenia XSI do domyślnych (nie -elub -Ewymagane) rozszerzenia mandatu POSIX dla takich nazw.
Charles Duffy

0

Zrobiłem to z podstawieniem parametrów zmiennych po wczytaniu ścieżki za pomocą read -e (między innymi). Dzięki temu użytkownik może uzupełnić ścieżkę za pomocą tabulatora, a jeśli użytkownik wprowadzi ścieżkę ~, zostanie ona posortowana.

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

Dodatkową korzyścią jest to, że jeśli nie ma tyldy, nic się nie dzieje ze zmienną, a jeśli istnieje tylda, ale nie na pierwszej pozycji, jest ona również ignorowana.

(Dołączam -i do odczytu, ponieważ używam tego w pętli, aby użytkownik mógł naprawić ścieżkę, jeśli wystąpi problem).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.