Cara memperluas variabel khusus secara manual (mis: ~ tilde) di bash

135

Saya memiliki variabel dalam skrip bash saya yang nilainya seperti ini:

~/a/b/c

Perhatikan bahwa ini adalah tilde yang tidak dikembangkan. Ketika saya melakukan ls -lt pada variabel ini (sebut saja $ VAR), saya tidak mendapatkan direktori seperti itu. Saya ingin membiarkan bash menafsirkan / memperluas variabel ini tanpa menjalankannya. Dengan kata lain, saya ingin bash menjalankan eval tetapi tidak menjalankan perintah yang dievaluasi. Apakah ini mungkin di bash?

Bagaimana saya bisa meneruskan ini ke skrip saya tanpa ekspansi? Saya melewati argumen di sekitarnya dengan tanda kutip ganda.

Coba perintah ini untuk melihat apa yang saya maksud:

ls -lt "~"

Ini persis situasi saya. Saya ingin tilde diperluas. Dengan kata lain, apa yang harus saya ganti dengan sihir untuk membuat kedua perintah ini identik:

ls -lt ~/abc/def/ghi

dan

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

Perhatikan bahwa ~ / abc / def / ghi mungkin ada atau tidak ada.

madiyaan damha
sumber
4
Anda mungkin menemukan ekspansi Tilde dalam tanda kutip juga membantu. Sebagian besar, tetapi tidak sepenuhnya, menghindari penggunaan eval.
Jonathan Leffler
2
Bagaimana variabel Anda ditugaskan dengan tilde yang tidak dikembangkan? Mungkin yang diperlukan hanyalah menetapkan variabel itu dengan tanda kutip luar tilde. foo=~/"$filepath"ataufoo="$HOME/$filepath"
Chad Skeeters
dir="$(readlink -f "$dir")"
Jack Wasey

Jawaban:

98

Karena sifat StackOverflow, saya tidak bisa hanya membuat jawaban ini tidak dapat diterima, tetapi dalam 5 tahun sejak saya memposting ini, ada jawaban yang jauh lebih baik daripada jawaban saya yang dianggap sederhana dan sangat buruk (saya masih muda, jangan bunuh saya).

Solusi lain di utas ini adalah solusi yang lebih aman dan lebih baik. Lebih disukai, saya akan memilih salah satu dari dua ini:


Jawaban asli untuk tujuan bersejarah (tapi tolong jangan gunakan ini)

Jika saya tidak salah, "~"tidak akan diperluas dengan skrip bash dengan cara itu karena diperlakukan sebagai string literal "~". Anda dapat memaksa ekspansi melalui evalseperti ini.

#!/bin/bash

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

Atau, gunakan saja ${HOME}jika Anda ingin direktori home pengguna.

wkl
sumber
3
Apakah Anda memiliki perbaikan ketika variabel memiliki ruang di dalamnya?
Hugo
34
Saya menemukan yang ${HOME}paling menarik. Apakah ada alasan untuk tidak menjadikan ini sebagai rekomendasi utama Anda? Bagaimanapun, terima kasih!
bijak
1
+1 - Saya perlu memperluas ~ $ some_other_user dan eval berfungsi dengan baik ketika $ HOME tidak akan berfungsi karena saya tidak memerlukan rumah pengguna saat ini.
olivecoder
11
Menggunakan evaladalah saran yang mengerikan, itu benar-benar buruk karena mendapat begitu banyak upvotes. Anda akan mengalami segala macam masalah ketika nilai variabel berisi karakter meta shell.
user2719058
1
Saya tidak dapat menyelesaikan komentar saya pada waktu itu dan, kemudian, saya tidak diizinkan untuk mengeditnya nanti. Jadi saya bersyukur (terima kasih lagi @birryree) untuk solusi ini karena membantu dalam konteks spesifik saya saat itu. Terima kasih Charles karena membuat saya sadar.
olivecoder
114

Jika variabel vardimasukkan oleh pengguna, evalsebaiknya tidak digunakan untuk memperluas tilde menggunakan

eval var=$var  # Do not use this!

Alasannya adalah: pengguna dapat secara tidak sengaja (atau sengaja) mengetik misalnya var="$(rm -rf $HOME/)"dengan kemungkinan konsekuensi yang membahayakan.

Cara yang lebih baik (dan lebih aman) adalah dengan menggunakan ekspansi parameter Bash:

var="${var/#\~/$HOME}"
Håkon Hægland
sumber
8
Bagaimana Anda bisa mengubah ~ userName / bukan hanya ~ /?
aspergillusOryzae
3
Apa tujuan dari #di "${var/#\~/$HOME}"?
Jahid
3
@ Janid Ini dijelaskan dalam manual . Ini memaksa tilde hanya cocok di awal $var.
Håkon Hægland
1
Terima kasih. (1) Mengapa kita perlu \~melarikan diri ~? (2) Balasan Anda menganggap itu ~adalah karakter pertama di $var. Bagaimana kita bisa mengabaikan ruang putih terdepan $var?
Tim
1
@Tim Terima kasih atas komentarnya. Ya Anda benar, kami tidak perlu melarikan diri dari tilde kecuali itu adalah karakter pertama dari string yang tidak dikutip atau mengikuti a :dalam string yang tidak dikutip. Informasi lebih lanjut dalam dokumen . Untuk menghapus spasi putih terdepan, lihat Cara memangkas spasi putih dari variabel Bash?
Håkon Hægland
24

Merencanakan diri saya dari jawaban sebelumnya , untuk melakukan ini dengan kuat tanpa risiko keamanan yang terkait dengan 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%:}"
}

...digunakan sebagai...

path=$(expandPath '~/hello')

Sebagai alternatif, pendekatan yang lebih sederhana yang menggunakan evaldengan hati-hati:

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
sumber
4
Melihat kode Anda, sepertinya Anda menggunakan meriam untuk membunuh nyamuk. Ada punya untuk menjadi cara yang lebih sederhana ..
Gino
2
@Gino, pasti ada cara yang lebih sederhana; pertanyaannya adalah apakah ada cara sederhana yang juga aman.
Charles Duffy
2
@Gino, ... saya tidak kira yang dapat digunakan printf %quntuk melarikan diri segala sesuatu tetapi tilde, dan kemudian menggunakan evaltanpa risiko.
Charles Duffy
1
@Gino, ... dan seterusnya diimplementasikan.
Charles Duffy
3
Aman, ya, tapi sangat tidak lengkap. Kode saya tidak rumit untuk bersenang-senang - itu rumit karena operasi aktual yang dilakukan oleh ekspansi tilde kompleks.
Charles Duffy
10

Cara aman untuk menggunakan eval adalah "$(printf "~/%q" "$dangerous_path")". Perhatikan bahwa spesifik bash.

#!/bin/bash

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

Lihat pertanyaan ini untuk detailnya

Juga, perhatikan bahwa di bawah zsh ini akan sesederhana echo ${~dangerous_path}

eddygeek
sumber
echo ${~root}beri saya tidak ada output pada zsh (mac os x)
Orwellophile
export test="~root/a b"; echo ${~test}
Gyscos
9

Bagaimana dengan ini:

path=`realpath "$1"`

Atau:

path=`readlink -f "$1"`
Jay
sumber
terlihat bagus, tetapi jalan tidak ada di mac saya. Dan Anda harus menulis path = $ (realpath "$ 1")
Hugo
Hai @Hugo. Anda dapat mengkompilasi sendiri realpathperintah dalam C. Misalnya, Anda dapat menghasilkan executable realpath.exemenggunakan pesta dan gcc dari baris perintah ini: 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 tidak benar, setidaknya di linux: realpath ~->/home/myhome
blueFast
iv'e menggunakannya dengan menyeduh pada mac
nhed
1
@dangonfast Ini tidak akan berfungsi jika Anda menetapkan tanda gelombang menjadi tanda kutip, hasilnya adalah <workingdir>/~.
Murphy
7

Memperluas (no pun intended) pada jawaban birryree dan halloleo: Pendekatan umum adalah menggunakan eval, tetapi ia datang dengan beberapa peringatan penting, yaitu spasi dan redirection output ( >) dalam variabel. Sepertinya ini bekerja untuk saya:

mypath="$1"

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

Cobalah dengan masing-masing argumen berikut:

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

Penjelasan

  • The ${mypath//>}strip keluar >karakter yang bisa mengkritik file selama eval.
  • Inilah eval echo ...yang dilakukan ekspansi tilde yang sebenarnya
  • Kutipan ganda di sekitar -eargumen adalah untuk mendukung nama file dengan spasi.

Mungkin ada solusi yang lebih elegan, tapi ini yang bisa saya lakukan.

Noach Magedman
sumber
3
Anda mungkin mempertimbangkan untuk melihat perilaku dengan nama yang mengandung $(rm -rf .).
Charles Duffy
1
Tidakkah ini memecah jalur yang benar-benar berisi >karakter?
Radon Rosborough
2

Saya percaya ini yang Anda cari

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

Contoh penggunaan:

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

$ magic ~invalid/this/will/not/expand
~invalid/this/will/not/expand
Orwellophile
sumber
Saya terkejut bahwa printf %q tidak luput dari tildes terkemuka - hampir tergoda untuk mengajukan ini sebagai bug, karena ini adalah situasi di mana ia gagal pada tujuan yang dinyatakan. Namun, untuk sementara, panggilan yang bagus!
Charles Duffy
1
Sebenarnya - bug ini diperbaiki di beberapa titik antara 3.2.57 dan 4.3.18, jadi kode ini tidak lagi berfungsi.
Charles Duffy
1
Poin baiknya, saya telah menyesuaikan kode untuk menghapus yang memimpin \ jika ada, jadi semuanya tetap dan berfungsi :) Saya menguji tanpa mengutip argumen, jadi itu berkembang sebelum memanggil fungsi.
Orwellophile
1

Inilah solusi saya:

#!/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
sumber
Juga, mengandalkan echocara yang expandTilde -ntidak akan berperilaku seperti yang diharapkan, dan perilaku dengan nama file yang mengandung garis miring terbalik tidak ditentukan oleh POSIX. Lihat pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html
Charles Duffy
Tangkapan yang bagus. Saya biasanya menggunakan mesin satu pengguna jadi saya tidak berpikir untuk menangani kasus itu. Tapi saya pikir fungsinya dapat dengan mudah ditingkatkan untuk menangani kasus lain ini dengan melihat melalui file / etc / passwd untuk pengguna lain. Saya akan meninggalkannya sebagai latihan untuk orang lain :).
Gino
Saya sudah melakukan latihan itu (dan menangani kasus OLDPWD dan lainnya) dalam jawaban yang Anda anggap terlalu rumit. :)
Charles Duffy
sebenarnya, saya baru saja menemukan solusi satu-baris yang cukup sederhana yang harus menangani kasus pengguna lain: path = $ (eval echo $ orgPath)
Gino
1
FYI: Saya baru saja memperbarui solusi saya sehingga sekarang dapat menangani ~ nama pengguna dengan benar. Dan, itu seharusnya cukup aman juga. Bahkan jika Anda memasukkan '/ tmp / $ (rm -rf / *)' sebagai argumen, itu harus menanganinya dengan anggun.
Gino
1

Cukup gunakan evaldengan benar: dengan validasi.

case $1${1%%/*} in
([!~]*|"$1"?*[!-+_.[:alnum:]]*|"") ! :;;
(*/*)  set "${1%%/*}" "${1#*/}"       ;;
(*)    set "$1" 
esac&& eval "printf '%s\n' $1${2+/\"\$2\"}"
mikeserv
sumber
Ini mungkin aman - saya belum menemukan kasus yang gagal. Yang mengatakan, jika kita akan berbicara dengan menggunakan eval "dengan benar", saya berpendapat bahwa jawaban Orwellophile mengikuti praktik yang lebih baik: Saya percaya shell printf %quntuk menghindari hal-hal lebih aman daripada saya percaya kode validasi tulisan tangan tidak memiliki bug .
Charles Duffy
@ Charles Duffy - itu konyol. shell mungkin tidak memiliki% q - dan printfmerupakan $PATHperintah 'd.
mikeserv
1
Bukankah pertanyaan ini ditandai bash? Jika demikian, printfadalah builtin, dan %qdijamin akan hadir.
Charles Duffy
@ Charles Duffy - versi apa?
mikeserv
1
@ Charles Duffy - itu ... cukup awal. tetapi saya masih berpikir itu aneh bahwa Anda akan mempercayai% q arg lebih dari yang Anda kode tepat di depan mata Anda, ive digunakan bashcukup sebelumnya untuk tahu untuk tidak mempercayainya. coba:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeserv
1

Berikut adalah fungsi POSIX yang setara dengan jawaban Bash Håkon Hægland

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

Sunting 2017-12-10: tambahkan '%s'per @CharlesDuffy di komentar.

go2null
sumber
1
printf '%s\n' "$tilde_less"mungkin? Kalau tidak, itu akan berperilaku salah jika nama file yang diperluas mengandung garis miring terbalik %s,, atau sintaksis lain yang bermakna bagi printf. Selain itu, meskipun, ini adalah jawaban yang bagus - benar (ketika ekstensi bash / ksh tidak perlu dibahas), jelas aman (tidak mucking dengan eval) dan singkat.
Charles Duffy
1

mengapa tidak menggali langsung ke direktori home pengguna dengan getent?

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

Hanya untuk memperpanjang jawaban birryree untuk jalur dengan spasi: Anda tidak dapat menggunakan evalperintah apa adanya karena memisahkan evaluasi dengan spasi. Salah satu solusinya adalah mengganti spasi sementara untuk perintah 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

Contoh ini tentu saja bergantung pada asumsi yang mypathtidak pernah berisi urutan char "_spc_".

halo
sumber
1
Tidak bekerja dengan tab, atau baris baru, atau apa pun di IFS ... dan tidak memberikan keamanan di sekitar karakter metak seperti jalur yang mengandung$(rm -rf .)
Charles Duffy
0

Anda mungkin menemukan ini lebih mudah dilakukan dengan python.

(1) Dari baris perintah unix:

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

Hasil dalam:

/Users/someone/fred

(2) Dalam skrip bash sebagai satu kali - simpan ini sebagai test.sh:

#!/usr/bin/env bash

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

echo $thepath

Menjalankan bash ./test.shhasil dalam:

/Users/someone/fred

(3) Sebagai utilitas - simpan ini di expandusersuatu tempat di jalur Anda, dengan menjalankan izin:

#!/usr/bin/env python

import sys
import os

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

Ini kemudian dapat digunakan pada baris perintah:

expanduser ~/fred

Atau dalam naskah:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath
Chris Johnson
sumber
Atau bagaimana kalau hanya meneruskan '~' ke Python, mengembalikan "/ home / fred"?
Tom Russell
1
Membutuhkan kutipan moar. echo $thepathbuggy; perlu untuk echo "$thepath"memperbaiki kasus-kasus yang kurang umum (nama dengan tab atau run spasi dikonversi menjadi spasi tunggal; nama dengan gumpalan diperluas), atau printf '%s\n' "$thepath"untuk memperbaiki yang tidak umum juga (mis. file bernama -n, atau file dengan backslash literals pada sistem yang sesuai dengan XSI). Demikian pula,thepath=$(expanduser "$1")
Charles Duffy
... untuk memahami apa yang saya maksud tentang backslash literal, lihat pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html - POSIX memungkinkan echountuk berperilaku dengan cara yang sepenuhnya ditentukan oleh implementasi jika ada argumen yang mengandung backslash; ekstensi XSI opsional untuk POSIX mengamanatkan perilaku ekspansi default (tidak -eatau -Ediperlukan) untuk nama-nama tersebut.
Charles Duffy
0

Sederhana : ganti 'ajaib' dengan 'eval echo'.

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

Masalah: Anda akan mengalami masalah dengan variabel lain karena eval itu jahat. Misalnya:

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

Perhatikan bahwa masalah injeksi tidak terjadi pada ekspansi pertama. Jadi jika Anda hanya menggantinya magicdengan eval echo, Anda harus baik-baik saja. Tetapi jika Anda melakukannya echo $(eval echo ~), itu akan rentan terhadap injeksi.

Demikian pula, jika Anda melakukannya eval echo ~bukan eval echo "~", itu akan dihitung sebagai dua kali diperluas dan oleh karena itu injeksi akan segera mungkin dilakukan.

Karim Alibhai
sumber
1
Bertentangan dengan apa yang Anda katakan, kode ini tidak aman . Misalnya, tes s='echo; EVIL_COMMAND'. (Ini akan gagal karena EVIL_COMMANDtidak ada di komputer Anda. Tetapi jika perintah itu rm -r ~misalnya, itu akan menghapus direktori home Anda.)
Konrad Rudolph
0

Saya telah melakukan ini dengan substitusi parameter variabel setelah membaca di jalan menggunakan read -e (antara lain). Jadi pengguna dapat melakukan tab-melengkapi path, dan jika pengguna memasukkan ~ path itu akan diurutkan.

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

Manfaat tambahan adalah bahwa jika tidak ada tilde tidak ada yang terjadi pada variabel, dan jika ada tilde tetapi tidak di posisi pertama itu juga diabaikan.

(Saya menyertakan -i untuk dibaca karena saya menggunakan ini dalam satu lingkaran sehingga pengguna dapat memperbaiki jalur jika ada masalah.)

JamesIsIn
sumber