Bash'de özel bir değişkeni (örn: ~ tilde) manuel olarak genişletme


135

Bash betiğimde değeri şuna benzer bir değişken var:

~/a/b/c

Genişletilmemiş tilde olduğuna dikkat edin. Bu değişken üzerinde ls -lt yaptığımda (buna $ VAR deyin), böyle bir dizin alamıyorum. Bash'in bu değişkeni çalıştırmadan yorumlamasına / genişletmesine izin vermek istiyorum. Diğer bir deyişle, bash'in eval çalıştırmasını ancak değerlendirilen komutu çalıştırmamasını istiyorum. Bash'de bu mümkün mü?

Bunu genişletme olmadan betiğime nasıl aktarmayı başardım? Çevreleyen argümanı çift tırnakla geçtim.

Ne demek istediğimi görmek için bu komutu deneyin:

ls -lt "~"

Bu tam olarak içinde bulunduğum durum. Tilde'nin genişletilmesini istiyorum. Başka bir deyişle, bu iki komutu aynı yapmak için sihri neyle değiştirmeliyim:

ls -lt ~/abc/def/ghi

ve

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

~ / Abc / def / ghi'nin var olabileceğini veya olmayabileceğini unutmayın.


4
Alıntılardaki Tilde genişlemesini de yararlı bulabilirsiniz . Çoğunlukla, ancak tamamen değil, kullanmaktan kaçınır eval.
Jonathan Leffler

2
Değişkeninize genişletilmemiş bir yaklaşık işareti nasıl atandı? Belki de gerekli olan tek şey, bu değişkene tırnak işaretlerinin dışındaki tilde atamaktır. foo=~/"$filepath"veyafoo="$HOME/$filepath"
Chad Skeeters

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

Yanıtlar:


98

StackOverflow'un doğası gereği, bu cevabı kabul edilmeyecek hale getiremem, ancak bunu yayınladığımdan beri geçen 5 yıl içinde, kuşkusuz ilkel ve oldukça kötü cevabımdan çok daha iyi cevaplar oldu (gençtim, öldürme ben mi).

Bu konudaki diğer çözümler daha güvenli ve daha iyi çözümlerdir. Tercihen şu ikisinden biriyle giderim:


Tarihsel amaçlar için orijinal cevap (ancak lütfen bunu kullanmayın)

Eğer yanılmıyorsam, "~"bu şekilde bir bash betiği tarafından genişletilmeyecektir, çünkü birebir dizge olarak değerlendirilir "~". Bu şekilde genişlemeye zorlayabilirsiniz eval.

#!/bin/bash

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

Alternatif olarak, ${HOME}kullanıcının ana dizinini istiyorsanız sadece kullanın .


3
Değişkenin içinde boşluk olduğu zaman için bir düzeltme var mı?
Hugo

34
${HOME}En çekici buldum . Bunu birincil tavsiyeniz yapmamak için herhangi bir neden var mı? Her durumda teşekkürler!
sage

1
+1 - ~ $ some_other_user'ı genişletmem gerekiyordu ve şu anki eve ihtiyacım olmadığı için $ HOME çalışmadığında iyi çalışıyor.
olivecoder

12
Kullanmak evalkorkunç bir öneri, bu kadar çok oy alması gerçekten kötü. Değişkenin değeri kabuk meta karakterleri içerdiğinde her türlü problemle karşılaşırsınız.
user2719058

1
Yorumumu o anda tamamlayamadım ve daha sonra düzenlememe izin verilmedi. Bu yüzden bu çözüm için minnettarım (tekrar teşekkürler @birryree) o zamanki özel bağlamıma yardımcı olduğu için. Beni haberdar ettiğin için teşekkürler Charles.
olivecoder

114

Değişken Eğer vargiriş kullanıcı tarafından ise, evalgerektiği değil kullanarak tilde genişletmek için kullanılabilir

eval var=$var  # Do not use this!

Nedeni şudur: kullanıcı tesadüfen (veya amaçla) olabilir, örneğin var="$(rm -rf $HOME/)" olası felaket sonuçlarla olabilir.

Daha iyi (ve daha güvenli) bir yol, Bash parametresi genişletmeyi kullanmaktır:

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

8
~ / Yerine ~ userName / nasıl değiştirilir?
aspergillusOryzae

3
Amacı nedir #içinde "${var/#\~/$HOME}"?
Cahid

4
@Jahid O açıklanmıştır manuel . Tilde'yi yalnızca başlangıcında eşleşmeye zorlar $var.
Håkon Hægland

1
Teşekkürler. (1) Neden \~ie kaçmaya ihtiyacımız var ~? (2) Cevabınız ~içindeki ilk karakterin olduğunu varsayar $var. Baştaki beyaz boşlukları nasıl görmezden gelebiliriz $var?
Tim

1
@Tim Yorum için teşekkürler. Evet haklısınız, tırnaksız bir dizenin ilk karakteri değilse veya tırnaksız bir dizedeki bir tilde'yi takip etmedikçe tilde'dan kaçmamıza gerek :yoktur. Dokümanlarda daha fazla bilgi . Baştaki beyaz boşluğu kaldırmak için Bash değişkeninden
Håkon Hægland

24

Önceki bir cevaptan kendimi rahatsız ediyorum ilişkili güvenlik riskleri olmadan bunu sağlam bir şekilde yapmakeval :

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%:}"
}

... olarak kullanıldı ...

path=$(expandPath '~/hello')

Alternatif olarak, evaldikkatlice kullanan daha basit bir yaklaşım :

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
Kodunuza baktığınızda, bir sivrisinek öldürmek için bir top kullanıyormuşsunuz gibi görünüyor. Orada var var çok daha basit bir yol olarak ..
Gino

2
@Gino, kesinlikle daha basit bir yolu var; soru, aynı zamanda güvenli olan daha basit bir yol olup olmadığıdır.
Charles Duffy

2
@Gino, ... Ben do birini kullanabilirsiniz varsayalım printf %qherşeyi ama tilde kaçmak ve daha sonra kullanmak evalrisksiz.
Charles Duffy

1
@Gino, ... ve böyle uygulandı.
Charles Duffy

4
Güvenli, evet, ama çok eksik. Kodum eğlence için karmaşık değil - karmaşık çünkü tilde genişletmesi tarafından yapılan gerçek işlemler karmaşık.
Charles Duffy

10

Eval kullanmanın güvenli bir yolu "$(printf "~/%q" "$dangerous_path")". Bunun bash'a özgü olduğunu unutmayın.

#!/bin/bash

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

Bu soruyu görün bakın

Ayrıca, zsh altında bunun şu kadar basit olacağını unutmayın: echo ${~dangerous_path}


echo ${~root}bana zsh (mac os x) üzerinde çıktı vermiyor
Orwellophile

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

9

Buna ne dersin:

path=`realpath "$1"`

Veya:

path=`readlink -f "$1"`

güzel görünüyor, ancak realpath mac'ımda yok. Ve yol = $ (realpath "$ 1") yazmanız gerekir
Hugo

Merhaba @Hugo. Kendi derlemek olabilir realpathC. İçin durumda komutu, bir yürütülebilir üretebilir realpath.exekullanarak Bash ve gcc bu komut satırından: 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);}'. Şerefe
olibre

@Quuxplusone doğru değil, en azından linux üzerinde: realpath ~->/home/myhome
blueFast

iv'e bunu mac'da brew ile kullandı
nhed

1
@dangonfast Tilde'ı tırnak içine alırsanız bu işe yaramaz, sonuç şudur <workingdir>/~.
Murphy

7

Birryree'nin ve halloleo'nun cevaplarını genişletmek (kelime anlamı yoktur): Genel yaklaşım kullanmaktır eval, ancak bazı önemli uyarılarla, yani >değişkendeki boşluklar ve çıktı yeniden yönlendirmesi ( ) ile birlikte gelir. Aşağıdakiler benim için çalışıyor gibi görünüyor:

mypath="$1"

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

Aşağıdaki argümanların her biriyle deneyin:

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

açıklama

  • İşlem sırasında bir dosyayı bozabilecek karakterleri ${mypath//>}çıkarır >.eval .
  • eval echo ...Gerçek yaklaşık yorumlaması yaptığı iştir
  • Bağımsız -edeğişken etrafındaki çift tırnak işaretleri , boşluk içeren dosya adlarını desteklemek içindir.

Belki daha zarif bir çözüm var, ama bulabildiğim şey buydu.


3
İçeren isimler içeren davranışa bakmayı düşünebilirsiniz $(rm -rf .).
Charles Duffy

1
>Yine de bu, aslında karakter içeren yollarda kopmaz mı?
Radon Rosborough

2

Bence aradığın bu

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

Örnek kullanım:

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

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

Bunu şaşırdım printf %q gelmez lider tilde kaçış - bu onun belirtilen amaç başarısız olduğu bir durum olarak neredeyse, bir hata olarak bu dosyaya cazip gelebilir. Ancak bu arada, iyi bir çağrı!
Charles Duffy

1
Aslında - bu hata 3.2.57 ile 4.3.18 arasında bir noktada düzeltildi, bu nedenle bu kod artık çalışmıyor.
Charles Duffy

1
İyi bir nokta, eğer varsa baştaki \ olanı kaldırmak için koda ayarladım, bu yüzden hepsi düzeltildi ve çalıştı :) Bağımsız değişkenlerden alıntı yapmadan test ediyordum, bu yüzden işlevi çağırmadan önce genişliyordu.
Orwellophile

1

İşte benim çözümüm:

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

Ayrıca, beklendiği gibi davranmayacak echoaraçlara güvenmek expandTilde -nve ters eğik çizgi içeren dosya adlarıyla davranış POSIX tarafından tanımlanmamıştır. Bkz pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy

İyi yakalama. Normalde tek kullanıcılı bir makine kullanıyorum, bu yüzden bu davayla ilgilenmeyi düşünmedim. Ama bence bu işlev, diğer kullanıcı için / etc / passwd dosyasından geçerek bu diğer durumu ele almak için kolayca geliştirilebilir. Bunu başkası için bir egzersiz olarak bırakacağım :).
Gino

Bu alıştırmayı zaten yaptım (ve OLDPWD vakasını ve diğerlerini) çok karmaşık gördüğünüz bir cevapla. :)
Charles Duffy

aslında, diğer kullanıcı durumunu ele alması gereken oldukça basit tek satırlık bir çözüm buldum: yol = $ (eval echo $ orgPath)
Gino

1
Bilginize: Çözümümü şimdi ~ kullanıcı adını doğru şekilde işleyebilmesi için güncelledim. Ve aynı zamanda oldukça güvenli olmalıdır. Bir argüman olarak '/ tmp / $ (rm -rf / *)' koysanız bile, onu zarif bir şekilde ele almalıdır.
Gino

1

Sadece evaldoğru şekilde kullanın : doğrulama ile.

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

Bu muhtemelen güvenlidir - başarısız olduğu bir dava bulamadım. Bununla birlikte, eval "doğru" kullanarak konuşacaksak, Orwellophile'ın cevabının daha iyi uygulamayı izlediğini iddia ediyorum: Kabuğun printf %qşeylerden güvenli bir şekilde kaçacağına, hata olmaması için elle yazılmış doğrulama koduna güvendiğimden daha fazla güveniyorum .
Charles Duffy

@Charles Duffy - bu çok aptalca. kabuk% q - içermeyebilir ve printfbir $PATH'd komutudur.
mikeserv

1
Bu soru etiketli değil bashmi? Eğer öyleyse, printfbir yerleşiktir ve %qmevcut olması garantilidir.
Charles Duffy

@Charles Duffy - hangi sürüm?
mikeserv

1
@Charles Duffy - bu ... oldukça erken. ama yine de a% q arg'ye gözünüzün önünde kodlayacağınızdan daha fazla güvenmeniz garip geliyor , güvenmemeyi bashbilmek için daha önce yeterince kullandım . deneyin:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv

1

İşte Håkon Hægland'ın Bash cevabının POSIX işlevi karşılığı

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

2017-12-10 düzenlemesi: '%s'yorumlarda @CharlesDuffy başına ekleyin .


1
printf '%s\n' "$tilde_less", belki? Aksi takdirde, genişletilen dosya adı ters eğik çizgi %sveya anlam ifade eden başka bir sözdizimi içeriyorsa hatalı davranır printf. Bunun dışında, bu harika bir cevap - doğru (bash / ksh uzantılarının kapsanması gerekmediğinde), açıkça güvenli (uğraşmadan eval) ve kısa.
Charles Duffy

1

neden doğrudan getent ile kullanıcının ana dizinine ulaşmayasınız?

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

0

Birryree'nin boşluklu yollar için yanıtını genişletmek için: Değerlendirmeyi boşluklarla ayırdığı için evalkomutu olduğu gibi kullanamazsınız . Bir çözüm, eval komutu için boşlukları geçici olarak değiştirmektir:

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

Bu örnek, elbette mypath, char dizisini asla içermeyen varsayımına dayanmaktadır "_spc_".


1
IFS'de sekmelerle, satırsonlarıyla veya başka herhangi bir şeyle çalışmaz ... ve aşağıdakileri içeren yollar gibi meta karakterlerin etrafında güvenlik sağlamaz$(rm -rf .)
Charles Duffy

0

Bunu python'da yapmayı daha kolay bulabilirsiniz.

(1) Unix komut satırından:

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

Sonuçlar:

/Users/someone/fred

(2) Tek seferlik bir bash komut dosyası içinde - bunu şu şekilde kaydedin test.sh:

#!/usr/bin/env bash

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

echo $thepath

Çalıştırılan bash ./test.shsonuçlar:

/Users/someone/fred

(3) Bir yardımcı program olarak - bunu farklı kaydedin expanduser yürütme izinleriyle yolunuzda bir yere kaydedin:

#!/usr/bin/env python

import sys
import os

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

Bu daha sonra komut satırında kullanılabilir:

expanduser ~/fred

Veya bir senaryoda:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath

Ya da Python'a yalnızca '~' geçip "/ home / fred" döndürmeye ne dersiniz?
Tom Russell

1
Moar tırnaklara ihtiyaç var. echo $thepathadamcağız; olması gerekir echo "$thepath", ya da; daha az nadir durumlarda düzeltmek için (globs bunları genişletilmiş sahip isimler sekme veya boşluk ishal ile isimleri tek boşluklar dönüştürülmek) printf '%s\n' "$thepath". (çok nadir olanları düzeltmek için yani dosya adında -nveya bir dosya ile XSI uyumlu sistemde ters eğik çizgi değişmez değerleri). Benzer şekilde,thepath=$(expanduser "$1")
Charles Duffy

... ters eğik çizgi ile ilgili ne demek istediğimi anlamak için, bkz. pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - echoHerhangi bir bağımsız değişken ters eğik çizgi içeriyorsa , POSIX tamamen uygulama tanımlı bir şekilde davranmaya izin verir ; POSIX için isteğe bağlı XSI uzantıları, bu tür adlar için varsayılan (yok -eveya -Egerekli) genişletme davranışlarını zorunlu kılar .
Charles Duffy

0

En basit : 'magic'i' eval echo 'ile değiştirin.

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

Problem: eval kötü olduğu için diğer değişkenlerle ilgili sorunlarla karşılaşacaksınız. Örneğin:

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

Enjeksiyon sorununun ilk genişletmede olmadığını unutmayın. Sadece değiştirmek için eğer öyleyse magicile eval echo, iyi olmalıdır. Ama yaparsanecho $(eval echo ~) , o enjeksiyona yatkın olur.

Benzer şekilde, eval echo ~bunun yerine yaparsanız eval echo "~", bu iki kat genişletilmiş olarak sayılır ve bu nedenle enjeksiyon hemen mümkün olur.


1
Söylediğinin aksine, bu kod güvenli değil . Örneğin, test edin s='echo; EVIL_COMMAND'. ( EVIL_COMMANDBilgisayarınızda olmadığı için başarısız olur . Ancak bu komut rm -r ~örneğin olsaydı, ana dizininizi silmiş olurdu.)
Konrad Rudolph

0

Bunu, read -e kullanarak yolda okuduktan sonra değişken parametre ikamesi ile yaptım (diğerleri arasında). Böylece kullanıcı yolu sekme ile tamamlayabilir ve eğer kullanıcı bir ~ yola girerse sıralanır.

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

Eklenen fayda, eğer tilde yoksa değişkene hiçbir şey olmaz ve tilde varsa, ancak ilk konumda yoksa, bunun da göz ardı edilmesidir.

(Bunu bir döngüde kullandığım için okumak için -i ekledim, böylece kullanıcı bir sorun varsa yolu düzeltebilir.)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.