Điều này cần bash 4.1 nếu bạn sử dụng {fd}
hoặc local -n
.
Phần còn lại sẽ hoạt động trong bash 3.x Tôi hy vọng. Tôi không hoàn toàn chắc chắn do printf %q
- đây có thể là một tính năng bash 4.
Tóm lược
Ví dụ của bạn có thể được sửa đổi như sau để lưu trữ hiệu ứng mong muốn:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
in như mong muốn:
hello
4
Lưu ý rằng giải pháp này:
- Hoạt động cho
e=1000
, quá.
- Bảo quản
$?
nếu bạn cần$?
Những tác động xấu duy nhất là:
- Nó cần một hiện đại
bash
.
- Nó fork thường xuyên hơn.
- Nó cần chú thích (được đặt tên theo chức năng của bạn, với một phần bổ sung
_
)
- Nó hy sinh bộ mô tả tệp 3.
- Bạn có thể thay đổi nó thành FD khác nếu bạn cần.
- Trong
_capture
chỉ cần thay thế tất cả các lần xuất hiện của 3
với một số (cao hơn).
Phần sau (khá dài, rất tiếc vì điều đó) hy vọng sẽ giải thích, cách gắn công thức này vào các tập lệnh khác.
Vấn đề
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
đầu ra
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
trong khi đầu ra mong muốn là
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Nguyên nhân của vấn đề
Các biến Shell (hay nói chung là môi trường) được chuyển từ các quy trình mẹ sang các quy trình con, nhưng không phải ngược lại.
Nếu bạn thực hiện thu thập dữ liệu đầu ra, điều này thường được chạy trong một vỏ con, do đó, việc chuyển lại các biến trở lại rất khó khăn.
Một số thậm chí nói với bạn, rằng nó không thể sửa chữa được. Điều này là sai, nhưng nó là một vấn đề khó giải được biết từ lâu.
Có một số cách về cách giải quyết nó tốt nhất, điều này phụ thuộc vào nhu cầu của bạn.
Đây là hướng dẫn từng bước về cách thực hiện.
Chuyển lại các biến vào shell cha
Có một cách để chuyển lại các biến cho một shell gốc. Tuy nhiên đây là một con đường nguy hiểm, bởi vì điều này sử dụng eval
. Nếu làm không đúng cách, bạn có nguy cơ gặp nhiều điều ác. Nhưng nếu được thực hiện đúng cách, điều này hoàn toàn an toàn, miễn là không có lỗi nào trong đó bash
.
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
bản in
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Lưu ý rằng điều này cũng có hiệu quả đối với những thứ nguy hiểm:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
bản in
; /bin/echo *
Điều này là do printf '%q'
, trích dẫn mọi thứ như vậy, mà bạn có thể sử dụng lại nó trong ngữ cảnh shell một cách an toàn.
Nhưng đây là một nỗi đau trong ..
Điều này không chỉ trông xấu xí mà còn phải đánh máy nhiều nên dễ bị lỗi. Chỉ một sai lầm duy nhất và bạn phải chịu đựng, phải không?
Chà, chúng tôi đang ở cấp độ vỏ, vì vậy bạn có thể cải thiện nó. Chỉ cần nghĩ về một giao diện bạn muốn xem và sau đó bạn có thể triển khai nó.
Tăng cường, cách shell xử lý mọi thứ
Hãy quay lại một bước và suy nghĩ về một số API cho phép chúng ta dễ dàng thể hiện những gì chúng ta muốn làm.
Chà, chúng ta muốn làm gì với d()
hàm?
Chúng tôi muốn nắm bắt đầu ra thành một biến. OK, sau đó hãy triển khai một API cho chính xác điều này:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Bây giờ, thay vì viết
d1=$(d)
chúng tôi có thể viết
capture d1 d
Chà, điều này có vẻ như chúng ta không thay đổi nhiều, vì một lần nữa, các biến không được chuyển trở lại từ d
trình bao mẹ và chúng ta cần phải nhập thêm một chút.
Tuy nhiên, bây giờ chúng ta có thể ném toàn bộ sức mạnh của shell vào nó, vì nó được gói gọn trong một hàm.
Hãy nghĩ về một giao diện dễ sử dụng lại
Điều thứ hai là chúng tôi muốn trở nên KHÔ (Không lặp lại chính mình). Vì vậy, chúng tôi chắc chắn không muốn nhập những thứ như
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
Ở x
đây không chỉ thừa, nó dễ bị lỗi luôn lặp lại trong ngữ cảnh chính xác. Điều gì sẽ xảy ra nếu bạn sử dụng nó 1000 lần trong một script và sau đó thêm một biến? Bạn chắc chắn không muốn thay đổi tất cả 1000 vị trí nơi một cuộc gọi đếnd
liên quan đến .
Vì vậy, hãy bỏ x
đi, để chúng ta có thể viết:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
đầu ra
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Điều này đã trông rất tốt. (Nhưng vẫn có local -n
cái không hoạt động trong oder chung bash
3.x)
Tránh thay đổi d()
Giải pháp cuối cùng có một số sai sót lớn:
d()
cần được thay đổi
- Nó cần phải sử dụng một số chi tiết bên trong
xcapture
để vượt qua đầu ra.
- Lưu ý rằng bóng đổ này (đốt cháy) một biến được đặt tên
output
, vì vậy chúng ta không bao giờ có thể chuyển lại biến này.
- Nó cần hợp tác với
_passback
Chúng ta có thể loại bỏ điều này không?
Tất nhiên, chúng tôi có thể! Chúng tôi đang ở trong một cái vỏ, vì vậy có mọi thứ chúng tôi cần để hoàn thành việc này.
Nếu bạn nhìn kỹ hơn một chút vào cuộc gọi, eval
bạn có thể thấy rằng chúng tôi có 100% quyền kiểm soát tại vị trí này. "Bên trong", eval
chúng ta nằm trong một vỏ con, vì vậy chúng ta có thể làm mọi thứ mình muốn mà không sợ làm điều gì đó xấu với vỏ mẹ.
Vâng, tốt, vì vậy hãy thêm một trình bao bọc khác, bây giờ trực tiếp bên trong eval
:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
bản in
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Tuy nhiên, điều này, một lần nữa, có một số nhược điểm lớn:
- Các
!DO NOT USE!
điểm đánh dấu ở đó, bởi vì có một điều kiện đường đua rất tồi tệ, bạn không thể nhìn thấy dễ dàng:
- Đây
>(printf ..)
là một công việc nền tảng. Vì vậy, nó có thể vẫn thực thi trong khi _passback x
đang chạy.
- Bạn có thể tự nhìn thấy điều này nếu bạn thêm
sleep 1;
trước printf
hoặc _passback
.
_xcapture a d; echo
sau đó đầu ra x
hoặc a
đầu tiên, tương ứng.
- Không
_passback x
nên là một phần của_xcapture
, vì điều này gây khó khăn cho việc sử dụng lại công thức đó.
- Ngoài ra, chúng tôi có một số ngã ba chưa được thực hiện ở đây (sự
$(cat)
), nhưng vì giải pháp này là !DO NOT USE!
tôi đã đi con đường ngắn nhất.
Tuy nhiên, điều này cho thấy rằng chúng ta có thể làm được điều đó mà không cần sửa đổi d()
(và không cầnlocal -n
)!
Xin lưu ý rằng chúng tôi không nhất thiết cần _xcapture
chút nào, vì chúng tôi có thể đã viết mọi nội dung ngay trongeval
.
Tuy nhiên làm điều này thường không dễ đọc. Và nếu bạn quay lại kịch bản của mình sau một vài năm, bạn có thể muốn đọc lại nó mà không gặp nhiều khó khăn.
Khắc phục cuộc đua
Bây giờ chúng ta hãy sửa điều kiện cuộc đua.
Bí quyết có thể là đợi cho đến khi printf
nó đóng STDOUT, và sau đó xuất ra x
.
Có nhiều cách để lưu trữ cái này:
- Bạn không thể sử dụng các đường ống shell, vì các đường ống chạy trong các quy trình khác nhau.
- Người ta có thể sử dụng các tệp tạm thời,
- hoặc một cái gì đó như một tập tin khóa hoặc một năm. Điều này cho phép chờ khóa hoặc năm mươi,
- hoặc các kênh khác nhau, để xuất thông tin, và sau đó lắp ráp đầu ra theo một số trình tự đúng.
Theo đường dẫn cuối cùng có thể trông như thế nào (lưu ý rằng nó là đường printf
cuối cùng vì nó hoạt động tốt hơn ở đây):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
đầu ra
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Tại sao điều này là chính xác?
_passback x
trực tiếp nói chuyện với STDOUT.
- Tuy nhiên, vì STDOUT cần được nắm bắt trong lệnh bên trong, trước tiên chúng tôi "lưu" nó vào FD3 (tất nhiên bạn có thể sử dụng những người khác) với '3> & 1' và sau đó sử dụng lại với
>&3
.
- Các
$("${@:2}" 3<&-; _passback x >&3)
kết thúc sau _passback
, khi vỏ con đóng STDOUT.
- Vì vậy,
printf
không thể xảy ra trước _passback
, bất kể _passback
mất bao lâu .
- Lưu ý rằng
printf
lệnh không được thực thi trước khi dòng lệnh hoàn chỉnh được lắp ráp, vì vậy chúng ta không thể thấy các đồ tạo tác từ đó printf
, cách printf
thực hiện một cách độc lập .
Do đó đầu tiên _passback
thực thi, sau đó printf
.
Điều này giải quyết cuộc đua, hy sinh một bộ mô tả tệp cố định 3. Tất nhiên, bạn có thể chọn một bộ mô tả tệp khác trong trường hợp FD3 không miễn phí trong shellcript của bạn.
Cũng xin lưu ý rằng 3<&-
điều này bảo vệ FD3 được chuyển đến hàm.
Làm cho nó chung chung hơn
_capture
chứa các bộ phận, thuộc về d()
, xấu, từ góc độ khả năng tái sử dụng. Làm thế nào để giải quyết điều này?
Vâng, hãy làm điều đó theo cách khác biệt bằng cách giới thiệu một thứ nữa, một hàm bổ sung, phải trả về những thứ phù hợp, được đặt tên theo hàm ban đầu với _
đính kèm.
Hàm này được gọi sau hàm thực và có thể làm tăng thêm mọi thứ. Bằng cách này, điều này có thể được đọc như một số chú thích, vì vậy nó rất dễ đọc:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
vẫn in
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Cho phép truy cập vào mã trả lại
Chỉ còn thiếu một chút:
v=$(fn)
đặt $?
thành những gì đã fn
trả lại. Vì vậy, bạn có thể cũng muốn điều này. Tuy nhiên, nó cần một số điều chỉnh lớn hơn:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
bản in
23 42 69 FAIL
Vẫn còn rất nhiều chỗ để cải thiện
_passback()
có thể được tách ra với passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
có thể được loại bỏ với capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
Giải pháp gây ô nhiễm bộ mô tả tệp (ở đây 3) bằng cách sử dụng nó trong nội bộ. Bạn cần ghi nhớ điều đó nếu tình cờ vượt qua FDs.
Lưu ý rằng bash
4.1 trở lên {fd}
phải sử dụng một số FD không được sử dụng.
(Có lẽ tôi sẽ thêm một giải pháp ở đây khi tôi xem xét.)
Lưu ý rằng đây là lý do tại sao tôi sử dụng để đặt nó trong các chức năng riêng biệt như _capture
, bởi vì việc nhồi nhét tất cả vào một dòng là có thể, nhưng khiến nó ngày càng khó đọc và khó hiểu
Có lẽ bạn cũng muốn nắm bắt STDERR của hàm được gọi. Hoặc bạn thậm chí muốn chuyển vào và chuyển ra nhiều hơn một bộ ký mã từ và đến các biến.
Tôi chưa có giải pháp nào, tuy nhiên đây là một cách để bắt nhiều hơn một FD , vì vậy chúng ta cũng có thể trả lại các biến theo cách này.
Cũng đừng quên:
Điều này phải gọi một hàm shell, không phải một lệnh bên ngoài.
Không có cách nào dễ dàng để chuyển các biến môi trường ra khỏi các lệnh bên ngoài. ( LD_PRELOAD=
Mặc dù với điều đó thì có thể!) Nhưng điều này sau đó là một cái gì đó hoàn toàn khác.
Những từ cuối
Đây không phải là giải pháp khả thi duy nhất. Đó là một ví dụ cho một giải pháp.
Như mọi khi bạn có nhiều cách để thể hiện mọi thứ trong vỏ. Vì vậy, hãy thoải mái cải thiện và tìm kiếm thứ gì đó tốt hơn.
Giải pháp được trình bày ở đây còn khá xa mới là hoàn hảo:
- Nó gần như không phải testet ở tất cả, vì vậy xin vui lòng bỏ qua lỗi chính tả.
- Có rất nhiều chỗ để cải thiện, xem ở trên.
- Nó sử dụng nhiều tính năng từ hiện đại
bash
, vì vậy có lẽ khó có thể chuyển sang các shell khác.
- Và có thể có một số điều kỳ quặc mà tôi chưa nghĩ đến.
Tuy nhiên, tôi nghĩ rằng nó khá dễ sử dụng:
- Chỉ thêm 4 dòng "thư viện".
- Chỉ thêm 1 dòng "chú thích" cho hàm shell của bạn.
- Chỉ tạm thời hy sinh một bộ mô tả tệp.
- Và mỗi bước phải dễ hiểu ngay cả nhiều năm sau đó.