Làm thế nào tôi có thể mở rộng một Tilde ~ Là một phần của biến?


12

Khi tôi mở một dấu nhắc bash và gõ:

$ set -o xtrace
$ x='~/someDirectory'
+ x='~/someDirectory'
$ echo $x
+ echo '~/someDirectory'
~/someDirectory

Tôi đã hy vọng rằng dòng thứ 5 ở trên sẽ đi + echo /home/myUsername/someDirectory. Có cách nào để làm việc này không? Trong tập lệnh Bash gốc của tôi, biến x thực sự được nhập từ dữ liệu từ tệp đầu vào, thông qua một vòng lặp như thế này:

while IFS= read line
do
    params=($line)
    echo ${params[0]}
done <"./someInputFile.txt"

Tuy nhiên, tôi nhận được một kết quả tương tự, echo '~/someDirectory'thay vì echo /home/myUsername/someDirectory.


Trong ZSH này là x='~'; print -l ${x} ${~x}. Tôi đã bỏ cuộc sau khi đào qua bashhướng dẫn một lúc.
thrig

@thrig: Đây không phải là một bashism, hành vi này là POSIX.
WhiteWinterWolf

Liên quan cực kỳ chặt chẽ (nếu không phải là bản dupe): unix.stackexchange.com/questions/151850/ mẹo
Kusalananda

@Kusalananda: Tôi không chắc đây là bản dupe vì lý do ở đây hơi khác: OP không kèm theo $ x giữa các trích dẫn khi lặp lại.
WhiteWinterWolf

Bạn không đặt dấu ngã vào tệp đầu vào. Vấn đề được giải quyết.
chepner

Câu trả lời:


11

Các POSIX tiêu chuẩn mở rộng áp đặt từ để được thực hiện theo thứ tự sau (nhấn mạnh là của tôi):

  1. Mở rộng Tilde (xem Mở rộng Tilde), mở rộng tham số (xem Mở rộng tham số), thay thế lệnh (xem Thay thế lệnh) và mở rộng số học (xem Mở rộng số học) sẽ được thực hiện, bắt đầu kết thúc. Xem mục 5 trong Nhận dạng mã thông báo.

  2. Việc tách trường (xem Chia tách trường) sẽ được thực hiện trên các phần của các trường được tạo bởi bước 1, trừ khi IFS là null.

  3. Mở rộng tên đường dẫn (xem Mở rộng tên đường dẫn) sẽ được thực hiện, trừ khi tập -f có hiệu lực.

  4. Trích dẫn loại bỏ (xem Trích dẫn loại bỏ) sẽ luôn luôn được thực hiện cuối cùng.

Điểm duy nhất khiến chúng ta quan tâm ở đây là điểm đầu tiên: như bạn có thể thấy mở rộng dấu ngã được xử lý trước khi mở rộng tham số:

  1. Shell cố gắng mở rộng dấu ngã echo $x, không có dấu ngã nào được tìm thấy, vì vậy nó tiến hành.
  2. Shell cố gắng mở rộng tham số echo $x, $xđược tìm thấy và mở rộng và dòng lệnh trở thành echo ~/someDirectory.
  3. Quá trình xử lý tiếp tục, mở rộng dấu ngã đã được xử lý ~nhân vật vẫn như cũ.

Bằng cách sử dụng dấu ngoặc kép trong khi gán $x, bạn rõ ràng yêu cầu không mở rộng dấu ngã và coi nó như một ký tự bình thường. Một điều thường bị bỏ lỡ là trong các lệnh shell, bạn không phải trích dẫn toàn bộ chuỗi, vì vậy bạn có thể thực hiện mở rộng ngay trong khi gán biến:

user@host:~$ set -o xtrace
user@host:~$ x=~/'someDirectory'
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Và bạn cũng có thể thực hiện việc mở rộng xảy ra trên dòng echolệnh miễn là nó có thể xảy ra trước khi mở rộng tham số:

user@host:~$ x='someDirectory'
+ x=someDirectory
user@host:~$ echo ~/$x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$

Nếu vì một lý do nào đó bạn thực sự cần ảnh hưởng đến dấu ngã $xmà không cần mở rộng và có thể mở rộng nó theo echolệnh, bạn phải tiến hành hai lần để buộc hai lần mở rộng của $xbiến xảy ra:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ echo "$( eval echo $x )"
++ eval echo '~/someDirectory'
+++ echo /home/user/someDirectory
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Tuy nhiên, lưu ý rằng tùy thuộc vào bối cảnh bạn sử dụng cấu trúc như vậy, nó có thể có tác dụng phụ không mong muốn. Theo nguyên tắc thông thường, thích tránh sử dụng bất cứ điều gì cần thiết evalkhi bạn có một cách khác.

Nếu bạn muốn giải quyết cụ thể vấn đề dấu ngã trái ngược với bất kỳ loại mở rộng nào khác, cấu trúc như vậy sẽ an toàn và di động hơn:

user@host:~$ x='~/someDirectory'
+ x='~/someDirectory'
user@host:~$ case "$x" in "~/"*)
>     x="${HOME}/${x#"~/"}"
> esac
+ case "$x" in
+ x=/home/user/someDirectory
user@host:~$ echo $x
+ echo /home/user/someDirectory
/home/user/someDirectory
user@host:~$ 

Cấu trúc này kiểm tra rõ ràng sự hiện diện của một hàng đầu ~và thay thế nó bằng thư mục nhà của người dùng nếu nó được tìm thấy.

Theo nhận xét của bạn, điều x="${HOME}/${x#"~/"}"này thực sự có thể gây ngạc nhiên cho một người không được sử dụng trong lập trình shell, nhưng thực tế lại được liên kết với cùng một quy tắc POSIX mà tôi đã trích dẫn ở trên.

Theo tiêu chuẩn POSIX, việc loại bỏ trích dẫn xảy ra sau cùng và việc mở rộng tham số xảy ra rất sớm. Do đó, ${#"~"}được đánh giá và mở rộng xa trước khi đánh giá các trích dẫn bên ngoài. Lần lượt, như được định nghĩa trong quy tắc mở rộng Thông số :

Trong mỗi trường hợp cần một giá trị của từ (dựa trên trạng thái của tham số, như được mô tả bên dưới), từ phải chịu sự mở rộng dấu ngã, mở rộng tham số, thay thế lệnh và mở rộng số học.

Do đó, phía bên phải của #toán tử phải được trích dẫn hoặc thoát đúng để tránh mở rộng dấu ngã.

Vì vậy, để nói khác đi, khi người phiên dịch vỏ nhìn vào x="${HOME}/${x#"~/"}", anh ta thấy:

  1. ${HOME}${x#"~/"}phải được mở rộng.
  2. ${HOME}được mở rộng đến nội dung của $HOMEbiến.
  3. ${x#"~/"}kích hoạt mở rộng lồng nhau: "~/"được phân tích cú pháp nhưng, được trích dẫn, được coi là nghĩa đen 1 . Bạn có thể đã sử dụng dấu ngoặc đơn ở đây với cùng kết quả.
  4. ${x#"~/"}bản thân biểu thức hiện được mở rộng, dẫn đến tiền tố ~/bị xóa khỏi giá trị của $x.
  5. Kết quả của những điều trên bây giờ được nối lại: sự mở rộng ${HOME}, nghĩa đen /, sự mở rộng ${x#"~/"}.
  6. Kết quả cuối cùng được đặt trong dấu ngoặc kép, có chức năng ngăn chặn việc tách từ. Tôi nói một cách chức năng ở đây bởi vì những trích dẫn kép này không bắt buộc về mặt kỹ thuật ( ví dụ ở đâyở đó ), nhưng như một phong cách cá nhân ngay khi một bài tập nhận được bất cứ điều gì ngoài a=$btôi thường thấy nó rõ ràng hơn khi thêm dấu ngoặc kép.

Nhân tiện, nếu nhìn kỹ hơn vào casecú pháp, bạn sẽ thấy "~/"*cấu trúc dựa trên cùng một khái niệm như x=~/'someDirectory'tôi đã giải thích ở trên (ở đây một lần nữa, các dấu ngoặc kép và đơn giản có thể được sử dụng thay thế cho nhau).

Đừng lo lắng nếu những điều này có vẻ mơ hồ ngay từ cái nhìn đầu tiên (thậm chí có thể ở tầm nhìn thứ hai hoặc sau đó!). Theo tôi, việc mở rộng tham số, với các lớp con, là một trong những khái niệm phức tạp nhất cần nắm bắt khi lập trình bằng ngôn ngữ shell.

Tôi biết rằng một số người có thể không đồng ý mạnh mẽ, nhưng nếu bạn muốn học lập trình shell sâu hơn, tôi khuyến khích bạn đọc Hướng dẫn về Script Bash nâng cao : nó dạy Bash-scripting, vì vậy với rất nhiều phần mở rộng và chuông - và- huýt sáo so với kịch bản shell POSIX, nhưng tôi thấy nó được viết tốt với vô số ví dụ thực tế. Khi bạn quản lý điều này, thật dễ dàng để hạn chế các tính năng POSIX khi bạn cần, cá nhân tôi nghĩ rằng việc nhập trực tiếp vào vương quốc POSIX là một đường cong học tập dốc không cần thiết cho người mới bắt đầu (so sánh thay thế dấu ngã POSIX của tôi với biểu thức chính quy của @ m0dular tương đương để có được một ý tưởng về những gì tôi muốn nói;)!).


1 : Điều này dẫn tôi đến việc tìm ra một lỗi trong Dash không thực hiện mở rộng dấu ngã ở đây một cách chính xác (có thể kiểm chứng bằng cách sử dụng x='~/foo'; echo "${x#~/}"). Mở rộng tham số là một lĩnh vực phức tạp cho cả người dùng và chính các nhà phát triển hệ vỏ!


Làm thế nào là bash shell phân tích dòng x="${HOME}/${x#"~/"}"? Nó trông giống như một nối của 3 chuỗi: "${HOME}/${x#", ~/, và "}". Vỏ có cho phép dấu ngoặc kép lồng nhau khi cặp dấu ngoặc kép bên trong nằm trong một ${ }khối không?
Andrew

@Andrew: Tôi đã hoàn thành câu trả lời của mình với thông tin bổ sung hy vọng giải quyết bình luận của bạn.
WhiteWinterWolf

Cảm ơn, đây là một câu trả lời tuyệt vời. Tôi đã học được một tấn từ việc đọc nó. Ước gì tôi có thể nâng cấp nó nhiều hơn một lần :)
Andrew

@WhiteWinterWolf: vẫn vậy, shell không thấy các trích dẫn lồng nhau cho dù kết quả là gì.
avp

6

Một câu trả lời có thể có:

eval echo "$x"

Vì bạn đang đọc đầu vào từ một tập tin, tôi sẽ không làm điều này.

Bạn có thể tìm kiếm và thay thế ~ bằng giá trị $ HOME, như thế này:

x='~/.config'
x="${x//\~/$HOME}"
echo "$x"

Đưa cho tôi:

/home/adrian/.config

Lưu ý rằng bản ${parameter/pattern/string}mở rộng là phần mở rộng Bash và có thể không có sẵn trong các shell khác.
WhiteWinterWolf

Thật. OP đã đề cập đến việc anh ấy đang sử dụng Bash, vì vậy tôi nghĩ đó là một câu trả lời thích hợp.
m0dular

Tôi đồng ý, miễn là người ta gắn bó với Bash tại sao không tận dụng hết lợi thế của nó (không phải ai cũng cần tính di động ở mọi nơi), nhưng thật đáng để lưu ý cho người dùng không phải Bash (ví dụ như một số bản phân phối được gửi bằng Dash thay vì Bash ) vì vậy người dùng bị ảnh hưởng không ngạc nhiên.
WhiteWinterWolf

Tôi đã tự do đề cập đến bài đăng của bạn trong phần lạc đề của tôi về sự khác biệt giữa phần mở rộng Bash và tập lệnh shell POSIX, vì tôi nghĩ rằng câu lệnh regex giống như dòng Bash của bạn so với casecấu trúc POSIX của tôi minh họa rõ cách kịch bản Bash thân thiện hơn với người dùng cho những người mới bắt đầu.
WhiteWinterWolf
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.