Làm thế nào chúng ta có thể chạy một lệnh được lưu trữ trong một biến?


35
$ ls -l /tmp/test/my\ dir/
total 0

Tôi đã tự hỏi tại sao các cách sau để chạy lệnh trên thất bại hoặc thành công?

$ abc='ls -l "/tmp/test/my dir"'

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0


Câu trả lời:


54

Điều này đã được thảo luận trong một số câu hỏi trên unix.SE, tôi sẽ cố gắng thu thập tất cả các vấn đề tôi có thể đưa ra ở đây. Tài liệu tham khảo ở cuối.


Tại sao nó thất bại

Lý do bạn phải đối mặt với những vấn đề đó là phân tách từ và thực tế là các trích dẫn được mở rộng từ các biến không đóng vai trò là trích dẫn, mà chỉ là các ký tự bình thường.

Các trường hợp được trình bày trong câu hỏi:

$ abc='ls -l "/tmp/test/my dir"'

Ở đây, $abcđược phân chia và lsnhận được hai đối số "/tmp/test/mydir"(với các trích dẫn ở phía trước của đầu tiên và phía sau của thứ hai):

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Ở đây, phần mở rộng được trích dẫn, vì vậy nó được giữ như một từ duy nhất. Shell cố gắng tìm một chương trình được gọi là ls -l "/tmp/test/my dir"dấu cách và dấu ngoặc kép.

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

Và ở đây, chỉ có từ đầu tiên hoặc $abcđược lấy làm đối số -c, vì vậy Bash chỉ chạy lstrong thư mục hiện tại. Nói cách khác là lập luận để bash, và được sử dụng để điền vào $0, $1vv

$ bash -c $abc
'my dir'

Với bash -c "$abc", và eval "$abc", có một bước xử lý shell bổ sung, giúp báo giá hoạt động, nhưng cũng khiến tất cả các mở rộng shell được xử lý lại , do đó, có nguy cơ vô tình chạy mở rộng lệnh từ dữ liệu do người dùng cung cấp, trừ khi bạn rất cẩn thận về trích dẫn.


Cách tốt hơn để làm điều đó

Hai cách tốt hơn để lưu trữ lệnh là a) sử dụng hàm thay vào đó, b) sử dụng biến mảng (hoặc tham số vị trí).

Sử dụng một chức năng:

Đơn giản chỉ cần khai báo một hàm với lệnh bên trong và chạy hàm như thể đó là một lệnh. Mở rộng các lệnh trong hàm chỉ được xử lý khi lệnh chạy, không phải khi nó được xác định và bạn không cần trích dẫn các lệnh riêng lẻ.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

Sử dụng một mảng:

Mảng cho phép tạo các biến nhiều từ trong đó các từ riêng lẻ chứa khoảng trắng. Ở đây, các từ riêng lẻ được lưu trữ dưới dạng các phần tử mảng riêng biệt và phần "${array[@]}"mở rộng mở rộng mỗi phần tử dưới dạng các từ shell riêng biệt:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

Cú pháp hơi kinh khủng, nhưng mảng cũng cho phép bạn xây dựng từng dòng lệnh từng mảnh một. Ví dụ:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

hoặc giữ các phần của dòng lệnh không đổi và sử dụng mảng chỉ điền vào một phần của nó, các tùy chọn hoặc tên tệp:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

Nhược điểm của mảng là chúng không phải là một tính năng tiêu chuẩn, do đó, các vỏ POSIX đơn giản (như dashmặc định /bin/shtrong Debian / Ubuntu) không hỗ trợ chúng (nhưng xem bên dưới). Tuy nhiên, Bash, ksh và zsh làm, vì vậy có khả năng hệ thống của bạn có một số shell hỗ trợ mảng.

Sử dụng "$@"

Trong các shell không hỗ trợ cho các mảng được đặt tên, người ta vẫn có thể sử dụng các tham số vị trí (mảng giả "$@") để giữ các đối số của lệnh.

Dưới đây phải là các bit script di động tương đương với các bit mã trong phần trước. Mảng được thay thế bằng "$@", danh sách các tham số vị trí. Cài đặt "$@"được thực hiện setvà dấu ngoặc kép xung quanh "$@"rất quan trọng (những nguyên nhân này khiến các yếu tố của danh sách được trích dẫn riêng lẻ).

Đầu tiên, chỉ cần lưu trữ một lệnh với các đối số trong "$@"và chạy nó:

set -- ls -l "/tmp/test/my dir"
"$@"

Điều kiện thiết lập các phần của các tùy chọn dòng lệnh cho một lệnh:

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "$@" -l
fi
set -- "$@" "$targetdir"

"$@"

Chỉ sử dụng "$@"cho các tùy chọn và toán hạng:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(Tất nhiên, "$@"thường chứa đầy các đối số cho chính tập lệnh, vì vậy bạn sẽ phải lưu chúng ở đâu đó trước khi tái sử dụng "$@".)


Cẩn thận với eval!

Như evalgiới thiệu một mức độ bổ sung của báo giá và xử lý mở rộng, bạn cần cẩn thận với đầu vào của người dùng. Ví dụ: điều này hoạt động miễn là người dùng không gõ bất kỳ dấu ngoặc đơn nào:

read -r filename
cmd="ls -l '$filename'"
eval "$cmd";

Nhưng nếu họ đưa ra đầu vào '$(uname)'.txt, tập lệnh của bạn sẽ vui vẻ chạy thay thế lệnh.

Một phiên bản có mảng miễn nhiễm với điều đó vì các từ được giữ riêng biệt trong toàn bộ thời gian, không có trích dẫn hoặc xử lý nào khác cho nội dung của filename.

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

Tài liệu tham khảo


2
bạn có thể nhận được xung quanh các điều trích dẫn bằng cách làm cmd="ls -l $(printf "%q" "$filename")". không đẹp, nhưng nếu người dùng đã chết khi sử dụng một eval, nó sẽ giúp ích. Nó cũng rất hữu ích cho việc gửi lệnh mặc dù những điều tương tự, chẳng hạn như ssh foohost "ls -l $(printf "%q" "$filename")", hoặc trong phần viết của câu hỏi này : ssh foohost "$cmd".
Patrick

Không liên quan trực tiếp, nhưng bạn đã mã hóa thư mục? Trong trường hợp đó, bạn có thể muốn xem bí danh. Một cái gì đó như: $ alias abc='ls -l "/tmp/test/my dir"'
Nhảy Bunny

4

Cách an toàn nhất để chạy lệnh (không tầm thường) là eval. Sau đó, bạn có thể viết lệnh như bạn sẽ làm trên dòng lệnh và nó được thực thi chính xác như thể bạn vừa nhập nó. Nhưng bạn phải trích dẫn mọi thứ.

Trường hợp đơn giản:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

trường hợp không đơn giản:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

3

Dấu hiệu trích dẫn thứ hai phá vỡ lệnh.

Khi tôi chạy:

abc="ls -l '/home/wattana/Desktop'"
$abc

Nó đã cho tôi một lỗi.

Nhưng khi tôi chạy

abc="ls -l /home/wattana/Desktop"
$abc

Không có lỗi gì cả

Không có cách nào để khắc phục điều này tại thời điểm đó (đối với tôi) nhưng bạn có thể tránh lỗi bằng cách không có khoảng trống trong tên thư mục.

Câu trả lời này cho biết lệnh eval có thể được sử dụng để sửa lỗi này nhưng nó không hoạt động với tôi :(


1
Vâng, nó hoạt động miễn là không cần ví dụ tên tệp có không gian được nhúng (hoặc không gian chứa các ký tự toàn cầu).
ilkkachu

0

Nếu điều này không làm việc với '', thì bạn nên sử dụng ``:

abc=`ls -l /tmp/test/my\ dir/`

Cập nhật cách tốt hơn:

abc=$(ls -l /tmp/test/my\ dir/)

Điều này lưu trữ kết quả của lệnh trong một biến. OP (lạ lùng) muốn lưu chính lệnh đó trong một biến. Oh, và bạn thực sự nên bắt đầu sử dụng $( command... )thay vì backticks.
roaima

Cảm ơn bạn rất nhiều vì đã làm rõ và lời khuyên!
balon

0

Làm thế nào về một python3 một lót?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
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.