Ini membutuhkan bash 4.1 jika Anda menggunakan {fd}
atau local -n
.
Selebihnya harus bekerja di bash 3.x saya harap. Saya tidak sepenuhnya yakin karena printf %q
- ini mungkin fitur bash 4.
Ringkasan
Contoh Anda dapat dimodifikasi sebagai berikut untuk mengarsipkan efek yang diinginkan:
# 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"
mencetak sesuai keinginan:
hello
4
Perhatikan bahwa solusi ini:
- Bekerja untuk
e=1000
juga.
- Mempertahankan
$?
jika Anda membutuhkan$?
Satu-satunya efek samping yang buruk adalah:
- Perlu modern
bash
.
- Ini lebih sering bercabang.
- Ini membutuhkan anotasi (dinamai menurut fungsi Anda, dengan tambahan
_
)
- Ini mengorbankan deskriptor file 3.
- Anda dapat mengubahnya ke FD lain jika Anda membutuhkannya.
- Dalam
_capture
hanya mengganti semua kejadian dari 3
dengan nomor lain (yang lebih tinggi).
Berikut ini (yang cukup panjang, maaf untuk itu) mudah-mudahan menjelaskan, bagaimana cara menambahkan resep ini ke skrip lain juga.
Masalah
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
keluaran
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
sedangkan keluaran yang diinginkan adalah
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Penyebab masalahnya
Variabel shell (atau secara umum, lingkungan) diteruskan dari proses orang tua ke proses anak, tetapi tidak sebaliknya.
Jika Anda melakukan pengambilan keluaran, ini biasanya dijalankan dalam subkulit, jadi sulit untuk meneruskan kembali variabel.
Beberapa bahkan memberi tahu Anda, bahwa tidak mungkin untuk memperbaikinya. Ini salah, tetapi sudah lama diketahui sulit untuk memecahkan masalah.
Ada beberapa cara untuk menyelesaikannya dengan sebaik-baiknya, ini tergantung kebutuhan Anda.
Berikut adalah panduan langkah demi langkah tentang cara melakukannya.
Meneruskan kembali variabel ke dalam cangkang induk
Ada cara untuk meneruskan kembali variabel ke cangkang induk. Bagaimanapun ini adalah jalan yang berbahaya, karena ini berguna eval
. Jika dilakukan dengan tidak benar, Anda mempertaruhkan banyak hal jahat. Tetapi jika dilakukan dengan benar, ini sangat aman, asalkan tidak ada bug yang masuk 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
cetakan
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Perhatikan bahwa ini juga berfungsi untuk hal-hal berbahaya:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
cetakan
; /bin/echo *
Hal ini disebabkan printf '%q'
, yang mengutip semuanya, sehingga Anda dapat menggunakannya kembali dalam konteks shell dengan aman.
Tapi ini menyebalkan di ..
Ini tidak hanya terlihat jelek, tapi juga banyak mengetik, jadi rawan kesalahan. Hanya satu kesalahan dan Anda dikutuk, bukan?
Ya, kami berada di level shell, jadi Anda bisa meningkatkannya. Pikirkan tentang antarmuka yang ingin Anda lihat, dan kemudian Anda dapat menerapkannya.
Augment, bagaimana shell memproses sesuatu
Mari mundur selangkah dan pikirkan tentang beberapa API yang memungkinkan kita mengekspresikan dengan mudah, apa yang ingin kita lakukan.
Nah, apa yang ingin kita lakukan dengan d()
fungsinya?
Kami ingin menangkap output menjadi variabel. Oke, mari kita terapkan API untuk hal ini:
# 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="$("$@")"
}
Sekarang, alih-alih menulis
d1=$(d)
kita bisa menulis
capture d1 d
Nah, ini sepertinya kita tidak banyak berubah, karena, sekali lagi, variabel tidak dikirimkan kembali d
ke dalam shell induk, dan kita perlu mengetik sedikit lagi.
Namun sekarang kita bisa menggunakan kekuatan penuh dari shell padanya, karena itu dibungkus dengan baik dalam sebuah fungsi.
Pikirkan tentang antarmuka yang mudah digunakan kembali
Kedua, kita ingin KERING (Don't Repeat Yourself). Jadi kami pasti tidak ingin mengetik sesuatu seperti
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
Di x
sini tidak hanya berlebihan, itu juga rawan kesalahan untuk selalu berulang dalam konteks yang benar. Bagaimana jika Anda menggunakannya 1000 kali dalam skrip dan kemudian menambahkan variabel? Anda pasti tidak ingin mengubah semua 1000 lokasi tempat panggilan ked
terlibat.
Jadi tinggalkan x
saja, jadi kita bisa menulis:
_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
keluaran
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Ini sudah terlihat sangat bagus. (Tapi masih ada local -n
yang tidak bekerja pada kesamaan bash
3.x)
Hindari berubah d()
Solusi terakhir memiliki beberapa kekurangan besar:
d()
perlu diubah
- Ini perlu menggunakan beberapa detail internal
xcapture
untuk meneruskan output.
- Perhatikan bahwa bayangan ini (membakar) satu variabel bernama
output
, jadi kita tidak akan pernah bisa melewatkan yang ini kembali.
- Itu perlu bekerja sama dengan
_passback
Bisakah kita menyingkirkan ini juga?
Tentu saja kita bisa! Kita berada dalam cangkang, jadi ada semua yang kita butuhkan untuk menyelesaikannya.
Jika Anda melihat lebih dekat ke panggilan tersebut, eval
Anda dapat melihat, bahwa kami memiliki kontrol 100% di lokasi ini. "Inside" the eval
we are in a subshell, so we can do everything we want tanpa takut melakukan sesuatu yang buruk pada parental shell.
Ya, bagus, jadi mari tambahkan pembungkus lain, sekarang langsung di dalam 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
cetakan
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Namun, ini, sekali lagi, memiliki beberapa kelemahan utama:
- The
!DO NOT USE!
spidol yang ada, karena ada kondisi lomba yang sangat buruk dalam hal ini, yang Anda tidak dapat melihat dengan mudah:
- Ini
>(printf ..)
adalah pekerjaan latar belakang. Jadi mungkin masih mengeksekusi saat _passback x
sedang berjalan.
- Anda dapat melihatnya sendiri jika menambahkan
sleep 1;
sebelum printf
atau _passback
.
_xcapture a d; echo
lalu keluaran x
atau a
pertama, masing-masing.
- The
_passback x
tidak harus menjadi bagian dari _xcapture
, karena ini membuat sulit untuk menggunakan kembali resep itu.
- Juga kami memiliki beberapa garpu tak terikat di sini (itu
$(cat)
), tetapi karena solusi ini !DO NOT USE!
saya mengambil rute terpendek.
Namun, ini menunjukkan, bahwa kita dapat melakukannya, tanpa modifikasi d()
(dan tanpa local -n
)!
Harap dicatat bahwa kami tidak perlu _xcapture
sama sekali, karena kami dapat menulis semuanya langsung di eval
.
Namun melakukan ini biasanya tidak terlalu mudah dibaca. Dan jika Anda kembali ke skrip Anda dalam beberapa tahun, Anda mungkin ingin dapat membacanya lagi tanpa banyak kesulitan.
Perbaiki balapan
Sekarang mari perbaiki kondisi balapan.
Triknya bisa menunggu sampai printf
ditutup STDOUT-nya, dan kemudian keluaran x
.
Ada banyak cara untuk mengarsipkannya:
- Anda tidak dapat menggunakan pipa shell, karena pipa berjalan dalam proses yang berbeda.
- Seseorang dapat menggunakan file sementara,
- atau sesuatu seperti file kunci atau fifo. Ini memungkinkan untuk menunggu kunci atau fifo,
- atau saluran yang berbeda, untuk mengeluarkan informasi, dan kemudian mengumpulkan hasilnya dalam urutan yang benar.
Mengikuti jalur terakhir akan terlihat seperti (perhatikan bahwa ini melakukan yang printf
terakhir karena ini bekerja lebih baik di sini):
_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
keluaran
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Mengapa ini benar?
_passback x
langsung berbicara dengan STDOUT.
- Namun, karena STDOUT perlu ditangkap di perintah bagian dalam, pertama-tama kita "menyimpannya" ke dalam FD3 (Anda dapat menggunakan yang lain, tentu saja) dengan '3> & 1' dan kemudian menggunakannya kembali dengan
>&3
.
- The
$("${@:2}" 3<&-; _passback x >&3)
selesai setelah _passback
, ketika subkulit menutup stdout.
- Jadi
printf
tidak bisa terjadi sebelum _passback
, berapa lama pun _passback
waktunya.
- Perhatikan bahwa
printf
perintah tidak dijalankan sebelum baris perintah lengkap dipasang, jadi kita tidak dapat melihat artefak dari printf
, secara independen bagaimana printf
diimplementasikan.
Oleh karena itu pertama-tama _passback
dieksekusi, lalu printf
.
Ini menyelesaikan perlombaan, mengorbankan satu file deskriptor tetap 3. Anda dapat, tentu saja, memilih deskriptor file lain dalam kasus, bahwa FD3 tidak gratis di shellscript Anda.
Harap perhatikan juga 3<&-
yang melindungi FD3 untuk diteruskan ke fungsi.
Jadikan lebih umum
_capture
mengandung bagian-bagian, yang dimiliki d()
, yang buruk, dari perspektif dapat digunakan kembali. Bagaimana mengatasi ini?
Nah, lakukan dengan cara yang berbeda dengan memperkenalkan satu hal lagi, fungsi tambahan, yang harus mengembalikan hal yang benar, yang dinamai sesuai fungsi asli dengan _
lampiran.
Fungsi ini dipanggil setelah fungsi sebenarnya, dan dapat menambah banyak hal. Dengan cara ini, ini dapat dibaca sebagai beberapa anotasi, sehingga sangat mudah dibaca:
_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
masih mencetak
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Izinkan akses ke kode kembali
Hanya ada sedikit yang hilang:
v=$(fn)
set $?
ke apa yang fn
dikembalikan. Jadi, Anda mungkin menginginkan ini juga. Perlu beberapa penyesuaian yang lebih besar, meskipun:
# 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
cetakan
23 42 69 FAIL
Masih banyak ruang untuk perbaikan
_passback()
bisa dieliminasi dengan passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
bisa dihilangkan dengan capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
Solusi mencemari deskriptor file (di sini 3) dengan menggunakannya secara internal. Anda perlu mengingatnya jika kebetulan lulus FD.
Perhatikan bahwa bash
4.1 dan yang lebih baru {fd}
harus menggunakan beberapa FD yang tidak digunakan.
(Mungkin saya akan menambahkan solusi di sini ketika saya datang.)
Perhatikan bahwa inilah mengapa saya menggunakan untuk meletakkannya dalam fungsi terpisah seperti _capture
, karena memasukkan semua ini ke dalam satu baris dimungkinkan, tetapi membuatnya semakin sulit untuk dibaca dan dipahami
Mungkin Anda ingin menangkap STDERR dari fungsi yang dipanggil juga. Atau Anda bahkan ingin meneruskan masuk dan keluar lebih dari satu penulis naskah dari dan ke variabel.
Saya belum memiliki solusi, namun berikut adalah cara untuk menangkap lebih dari satu FD , jadi kita mungkin dapat mengirimkan kembali variabel dengan cara ini juga.
Juga jangan lupa:
Ini harus memanggil fungsi shell, bukan perintah eksternal.
Tidak ada cara mudah untuk melewatkan variabel lingkungan dari perintah eksternal. (Dengan LD_PRELOAD=
itu seharusnya mungkin!) Tapi ini adalah sesuatu yang sama sekali berbeda.
Kata-kata terakhir
Ini bukan satu-satunya solusi yang mungkin. Ini adalah salah satu contoh solusi.
Seperti biasa, Anda memiliki banyak cara untuk mengekspresikan sesuatu di shell. Jadi jangan ragu untuk meningkatkan dan menemukan sesuatu yang lebih baik.
Solusi yang disajikan di sini masih jauh dari sempurna:
- Itu hampir bukan testet sama sekali, jadi mohon maafkan kesalahan ketik.
- Ada banyak ruang untuk perbaikan, lihat di atas.
- Ini menggunakan banyak fitur dari modern
bash
, jadi mungkin sulit untuk melakukan port ke shell lain.
- Dan mungkin ada beberapa keanehan yang belum saya pikirkan.
Namun menurut saya ini cukup mudah digunakan:
- Tambahkan hanya 4 baris "perpustakaan".
- Tambahkan hanya 1 baris "penjelasan" untuk fungsi shell Anda.
- Mengorbankan hanya satu deskriptor file untuk sementara.
- Dan setiap langkah harus mudah dipahami bahkan bertahun-tahun kemudian.