Cách mở rộng thủ công một biến đặc biệt (ví dụ: ~ dấu ngã) trong bash


135

Tôi có một biến trong tập lệnh bash có giá trị như sau:

~/a/b/c

Lưu ý rằng đó là dấu ngã chưa được mở rộng. Khi tôi thực hiện ls -lt trên biến này (gọi nó là $ VAR), tôi không nhận được thư mục nào như vậy. Tôi muốn để bash diễn giải / mở rộng biến này mà không thực hiện nó. Nói cách khác, tôi muốn bash chạy eval nhưng không chạy lệnh được đánh giá. Điều này có thể trong bash?

Làm thế nào tôi quản lý để chuyển điều này vào kịch bản của mình mà không cần mở rộng? Tôi đã thông qua các đối số xung quanh nó với dấu ngoặc kép.

Hãy thử lệnh này để xem ý tôi là gì:

ls -lt "~"

Đây chính xác là tình huống tôi đang gặp phải. Tôi muốn dấu ngã được mở rộng. Nói cách khác, tôi nên thay thế phép thuật bằng cách làm cho hai lệnh này giống hệt nhau:

ls -lt ~/abc/def/ghi

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

Lưu ý rằng ~ / abc / def / ghi có thể tồn tại hoặc không tồn tại.


4
Bạn có thể thấy mở rộng Tilde trong dấu ngoặc kép cũng hữu ích. Nó chủ yếu, nhưng không hoàn toàn, tránh sử dụng eval.
Jonathan Leffler

2
Làm thế nào mà biến của bạn được chỉ định với một dấu ngã chưa được mở rộng? Có lẽ tất cả những gì được yêu cầu là gán biến đó với dấu ngoặc ngoài dấu ngoặc kép. foo=~/"$filepath"hoặcfoo="$HOME/$filepath"
Chad Skeeter

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

Câu trả lời:


98

Do tính chất của StackOverflow, tôi không thể chấp nhận câu trả lời này, nhưng trong 5 năm kể từ khi tôi đăng bài này, đã có những câu trả lời tốt hơn nhiều so với câu trả lời thô sơ và khá tệ của tôi (tôi còn trẻ, đừng giết tôi).

Các giải pháp khác trong chủ đề này là giải pháp an toàn hơn và tốt hơn. Tốt nhất là tôi nên đi với một trong hai người sau:


Câu trả lời gốc cho mục đích lịch sử (nhưng vui lòng không sử dụng điều này)

Nếu tôi không nhầm, "~"sẽ không được mở rộng bằng tập lệnh bash theo cách đó vì nó được coi là một chuỗi ký tự "~". Bạn có thể buộc mở rộng thông qua evalnhư thế này.

#!/bin/bash

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

Ngoài ra, chỉ cần sử dụng ${HOME}nếu bạn muốn thư mục nhà của người dùng.


3
Bạn có sửa lỗi khi biến có một khoảng trắng trong đó không?
Hugo

34
Tôi thấy ${HOME}hấp dẫn nhất. Có bất kỳ lý do để không làm cho đề nghị chính của bạn? Trong mọi trường hợp, cảm ơn!
hiền nhân

1
+1 - Tôi cần mở rộng ~ $ some_other_user và eval hoạt động tốt khi $ HOME không hoạt động vì tôi không cần nhà người dùng hiện tại.
olivecoder

11
Sử dụng evallà một gợi ý khủng khiếp, thật tệ khi nó nhận được rất nhiều sự ủng hộ. Bạn sẽ gặp phải tất cả các loại vấn đề khi giá trị của biến chứa các ký tự meta shell.
dùng2719058

1
Tôi không thể hoàn thành bình luận của mình vào thời điểm đó và sau đó, tôi không được phép chỉnh sửa nó sau này. Vì vậy, tôi rất biết ơn (cảm ơn một lần nữa @birryree) cho giải pháp này vì nó đã giúp ích trong bối cảnh cụ thể của tôi tại thời điểm đó. Cảm ơn Charles đã cho tôi biết.
olivecoder

114

Nếu biến varlà đầu vào bởi người sử dụng, evalnên không được sử dụng để mở rộng dấu ngã sử dụng

eval var=$var  # Do not use this!

Lý do là: người dùng có thể vô tình (hoặc do mục đích) loại ví dụ var="$(rm -rf $HOME/)"với hậu quả tai hại có thể xảy ra.

Một cách tốt hơn (và an toàn hơn) là sử dụng mở rộng tham số Bash:

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

8
Làm thế nào bạn có thể thay đổi ~ userName / thay vì chỉ ~ /?
aspergillusOryzae

3
Mục đích của #trong là "${var/#\~/$HOME}"gì?
Jahid

3
@Jahid Nó được giải thích trong hướng dẫn . Nó buộc dấu ngã chỉ khớp vào lúc bắt đầu $var.
Håkon Hægland

1
Cảm ơn. (1) Tại sao chúng ta cần \~tức là thoát ~? (2) Câu trả lời của bạn cho rằng đó ~là ký tự đầu tiên $var. Làm thế nào chúng ta có thể bỏ qua các khoảng trắng hàng đầu trong $var?
Tim

1
@Tim Cảm ơn bạn đã bình luận. Có bạn đúng, chúng ta không cần phải thoát dấu ngã trừ khi đó là ký tự đầu tiên của chuỗi không được trích dẫn hoặc nó đang theo một :chuỗi không được trích dẫn. Thêm thông tin trong các tài liệu . Để xóa khoảng trắng hàng đầu, hãy xem Cách cắt khoảng trắng từ biến Bash?
Håkon Hægland

24

Tự tôn mình từ một câu trả lời trước , để làm điều này một cách mạnh mẽ mà không có rủi ro bảo mật liên quan đến 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%:}"
}

...được dùng như...

path=$(expandPath '~/hello')

Thay phiên, một cách tiếp cận đơn giản hơn sử dụng evalcẩn thận:

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
Nhìn vào mã của bạn, có vẻ như bạn đang sử dụng một khẩu súng thần công để giết một con muỗi. Phải một cách đơn giản hơn nhiều ..
Gino

2
@Gino, chắc chắn là một cách đơn giản hơn; câu hỏi là liệu có một cách đơn giản hơn mà cũng an toàn.
Charles Duffy

2
@Gino, ... Tôi làm giả sử rằng người ta có thể sử dụng printf %qđể thoát khỏi tất cả mọi thứ nhưng dấu ngã, và sau đó sử dụng evalmà không có nguy cơ.
Charles Duffy

1
@Gino, ... và cứ thế thực hiện.
Charles Duffy

2
An toàn, có, nhưng rất nhiều không đầy đủ. Mã của tôi không phức tạp cho niềm vui của nó - nó phức tạp vì các hoạt động thực tế được thực hiện bởi mở rộng dấu ngã rất phức tạp.
Charles Duffy

10

Một cách an toàn để sử dụng eval là "$(printf "~/%q" "$dangerous_path")". Lưu ý đó là bash cụ thể.

#!/bin/bash

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

Xem câu hỏi này để biết chi tiết

Ngoài ra, lưu ý rằng theo zsh, điều này sẽ đơn giản như echo ${~dangerous_path}


echo ${~root}không cho tôi đầu ra trên zsh (mac os x)
Orwellophile

export test="~root/a b"; echo ${~test}
Tòa nhà chọc trời

9

Còn cái này thì sao:

path=`realpath "$1"`

Hoặc là:

path=`readlink -f "$1"`

trông đẹp, nhưng realpath không tồn tại trên máy mac của tôi. Và bạn sẽ phải viết đường dẫn = $ (realpath "$ 1")
Hugo

Xin chào @Hugo. Bạn có thể biên dịch realpathlệnh của riêng bạn trong C. Chẳng hạn, bạn có thể tạo một tệp thực thi realpath.exebằng bashgcc từ dòng lệnh này : 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);}'. Chúc mừng
olibre

@Quuxplusone không đúng, ít nhất là trên linux: realpath ~->/home/myhome
blueFast

iv'e sử dụng nó với bia trên mac
nhed

1
@dangonfast Điều này sẽ không hoạt động nếu bạn đặt dấu ngã vào dấu ngoặc kép, kết quả là <workingdir>/~.
Murphy

7

Mở rộng (không có ý định chơi chữ) đối với câu trả lời của birryree và hallole: Cách tiếp cận chung là sử dụng eval, nhưng nó đi kèm với một số cảnh báo quan trọng, cụ thể là khoảng trắng và chuyển hướng đầu ra ( >) trong biến. Những điều sau đây có vẻ hiệu quả với tôi:

mypath="$1"

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

Hãy thử nó với từng đối số sau đây:

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

Giải trình

  • Các ký tự ${mypath//>}loại bỏ các >ký tự có thể ghi đè lên một tệp trong eval.
  • Việc eval echo ...mở rộng dấu ngã thực sự là gì
  • Các trích dẫn kép xung quanh -eđối số là để hỗ trợ tên tệp có dấu cách.

Có lẽ có một giải pháp thanh lịch hơn, nhưng đây là những gì tôi có thể nghĩ ra.


3
Bạn có thể xem xét hành vi với tên chứa $(rm -rf .).
Charles Duffy

1
Điều này không phá vỡ trên các đường dẫn thực sự có chứa các >ký tự, mặc dù?
Radon Rosborough

2

Tôi tin rằng đây là những gì bạn đang tìm kiếm

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

Ví dụ sử dụng:

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

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

Tôi ngạc nhiên rằng printf %q nó không thoát khỏi những dấu hiệu hàng đầu - hầu như rất khó để coi đây là một lỗi, vì đó là một tình huống mà nó thất bại trong mục đích đã nêu. Tuy nhiên, trong thời gian tạm thời, một cuộc gọi tốt!
Charles Duffy

1
Trên thực tế - lỗi này được sửa tại một số điểm trong khoảng từ 3.2.57 đến 4.3.18, vì vậy mã này không còn hoạt động.
Charles Duffy

1
Điểm hay, tôi đã điều chỉnh mã để loại bỏ hàng đầu \ nếu nó tồn tại, vì vậy tất cả đã được sửa và hoạt động :) Tôi đã thử nghiệm mà không trích dẫn các đối số, vì vậy nó đã mở rộng trước khi gọi hàm.
Orwellophile

1

Đây là giải pháp của tôi:

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

Ngoài ra, dựa vào các echophương tiện expandTilde -nsẽ không hoạt động như mong đợi và hành vi với tên tệp có dấu gạch chéo ngược không được xác định bởi POSIX. Xem pubs.opengroup.org/onlinepub/009604599/utilities/echo.html
Charles Duffy

Nắm bắt tốt. Tôi thường sử dụng máy một người dùng vì vậy tôi không nghĩ sẽ xử lý trường hợp đó. Nhưng tôi nghĩ rằng chức năng có thể dễ dàng được tăng cường để xử lý trường hợp khác này bằng cách truy cập tệp / etc / passwd cho người dùng khác. Tôi sẽ để nó như một bài tập cho người khác :).
Gino

Tôi đã thực hiện bài tập đó (và xử lý trường hợp OLDPWD và các trường hợp khác) trong câu trả lời mà bạn cho là quá phức tạp. :)
Charles Duffy

thực ra, tôi chỉ tìm thấy một giải pháp một dòng khá đơn giản để xử lý trường hợp người dùng khác: path = $ (eval echo $ orgPath)
Gino

1
FYI: Tôi vừa cập nhật giải pháp của mình để bây giờ có thể xử lý ~ tên người dùng chính xác. Và, nó cũng khá an toàn. Ngay cả khi bạn đặt '/ tmp / $ (rm -rf / *)' làm đối số, thì nó vẫn xử lý nó một cách duyên dáng.
Gino

1

Chỉ cần sử dụng evalchính xác: với xác nhận.

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

Điều này có thể an toàn - tôi chưa tìm thấy trường hợp nào thất bại. Điều đó nói rằng, nếu chúng ta sẽ nói về việc sử dụng eval "một cách chính xác", tôi cho rằng câu trả lời của Orwellophile tuân theo cách thực hành tốt hơn: Tôi tin rằng vỏ printf %qsẽ thoát khỏi mọi thứ một cách an toàn hơn là tôi tin rằng mã xác thực viết tay không có lỗi .
Charles Duffy

@ Charles Duffy - thật ngớ ngẩn. shell có thể không có% q - và printf$PATHlệnh 'd.
mikeerv

1
Không phải câu hỏi này được gắn thẻ bash? Nếu vậy, printflà một nội trang, và %qđược đảm bảo có mặt.
Charles Duffy

@Charles Duffy - phiên bản nào?
mikeerv

1
@Charles Duffy - đó là ... khá sớm. nhưng tôi vẫn nghĩ thật kỳ lạ khi bạn tin tưởng% q arg nhiều hơn bạn sẽ viết mã ngay trước mắt, tôi đã sử dụng bashđủ trước đó để biết không tin tưởng nó. thử:x=$(printf \\1); [ -n "$x" ] || echo but its not null!
mikeerv

1

Đây là hàm POSIX tương đương với câu trả lời Bash của Håkon Hægland

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

Chỉnh sửa 2017-12-10: thêm '%s'mỗi @CharlesDuffy trong các bình luận.


1
printf '%s\n' "$tilde_less", có lẽ? Nếu không, nó sẽ hoạt động sai nếu tên tệp được mở rộng chứa dấu gạch chéo ngược %shoặc cú pháp khác có ý nghĩa printf. Mặc dù vậy, đây là một câu trả lời tuyệt vời - chính xác (khi các phần mở rộng bash / ksh không cần phải được bảo hiểm), rõ ràng là an toàn (không có mucking với eval) và ngắn gọn.
Charles Duffy

1

Tại sao không đi thẳng vào việc lấy thư mục nhà của người dùng với getent?

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

0

Chỉ để mở rộng câu trả lời của birryree cho các đường dẫn có khoảng trắng: Bạn không thể sử dụng evallệnh vì nó tách biệt đánh giá theo khoảng trắng. Một giải pháp là tạm thời thay thế khoảng trắng cho lệnh 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

Ví dụ này tất nhiên dựa vào giả định mypathkhông bao giờ chứa chuỗi char "_spc_".


1
Không hoạt động với các tab, hoặc dòng mới hoặc bất kỳ thứ gì khác trong IFS ... và không cung cấp bảo mật xung quanh các siêu nhân vật như các đường dẫn có chứa$(rm -rf .)
Charles Duffy

0

Bạn có thể thấy điều này dễ dàng hơn để làm trong python.

(1) Từ dòng lệnh unix:

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

Kết quả trong:

/Users/someone/fred

(2) Trong một tập lệnh bash là một lần - hãy lưu tập lệnh này dưới dạng test.sh:

#!/usr/bin/env bash

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

echo $thepath

Chạy bash ./test.shkết quả trong:

/Users/someone/fred

(3) Là một tiện ích - lưu cái này expanduserở đâu đó trên đường dẫn của bạn, với quyền thực thi:

#!/usr/bin/env python

import sys
import os

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

Điều này sau đó có thể được sử dụng trên dòng lệnh:

expanduser ~/fred

Hoặc trong một kịch bản:

#!/usr/bin/env bash

thepath=$(expanduser $1)

echo $thepath

Hoặc làm thế nào về việc chỉ chuyển '~' cho Python, trả về "/ home / fred"?
Tom Russell

1
Cần báo giá moar. echo $thepathlà lỗi; cần phải echo "$thepath"sửa các trường hợp ít phổ biến hơn (tên có tab hoặc chạy khoảng trắng được chuyển đổi thành khoảng trắng đơn; tên có khối lượng được mở rộng) hoặc printf '%s\n' "$thepath"sửa lỗi không phổ biến (ví dụ: tệp có tên -nhoặc tệp dấu gạch chéo ngược trên hệ thống tuân thủ XSI). Tương tự,thepath=$(expanduser "$1")
Charles Duffy

... Để hiểu những gì tôi muốn nói về dấu gạch chéo ngược, hãy xem pubs.opengroup.org/onlinepub/009604599/utilities/echo.html - POSIX cho phép echohành xử theo cách hoàn toàn được xác định nếu thực hiện bất kỳ đối số nào có dấu gạch chéo ngược; các phần mở rộng XSI tùy chọn cho các hành vi mở rộng mặc định (không -ehoặc -Ecần thiết) của POSIX cho các tên đó.
Charles Duffy

0

Đơn giản nhất : thay thế 'ma thuật' bằng 'tiếng vang eval'.

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

Vấn đề: Bạn sẽ gặp vấn đề với các biến khác vì eval là xấu xa. Ví dụ:

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

Lưu ý rằng vấn đề tiêm không xảy ra trong lần mở rộng đầu tiên. Vì vậy, nếu bạn chỉ đơn giản là thay thế magicbằng eval echo, bạn sẽ ổn thôi. Nhưng nếu bạn làm thế echo $(eval echo ~), điều đó sẽ dễ bị tiêm.

Tương tự, nếu bạn làm eval echo ~thay vì eval echo "~", điều đó sẽ được tính là hai lần mở rộng và do đó có thể tiêm ngay lập tức.


1
Trái với những gì bạn nói, mã này không an toàn . Ví dụ, kiểm tra s='echo; EVIL_COMMAND'. (Nó sẽ thất bại vì EVIL_COMMANDkhông tồn tại trên máy tính của bạn. Nhưng nếu lệnh đó là rm -r ~ví dụ, nó sẽ xóa thư mục nhà của bạn.)
Konrad Rudolph

0

Tôi đã thực hiện điều này với sự thay thế tham số biến sau khi đọc trong đường dẫn bằng cách sử dụng read -e (trong số những người khác). Vì vậy, người dùng có thể hoàn thành tab đường dẫn và nếu người dùng nhập một đường dẫn ~ thì nó sẽ được sắp xếp.

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

Lợi ích bổ sung là nếu không có dấu ngã thì không có gì xảy ra với biến và nếu có dấu ngã nhưng không ở vị trí đầu tiên thì nó cũng bị bỏ qua.

(Tôi bao gồm -i để đọc vì tôi sử dụng điều này trong một vòng lặp để người dùng có thể sửa đường dẫn nếu có vấn đề.)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.