Bagaimana cara memodifikasi variabel global dalam fungsi di bash?

105

Saya sedang mengerjakan ini:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

Saya memiliki skrip seperti di bawah ini:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

Yang mengembalikan:

hello
4

Tetapi jika saya menetapkan hasil fungsi ke variabel, variabel global etidak diubah:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

ret=$(test1)

echo "$ret"
echo "$e"

Pengembalian:

hello
2

Saya pernah mendengar tentang penggunaan eval dalam kasus ini, jadi saya melakukan ini di test1:

eval 'e=4'

Tapi hasilnya sama.

Bisakah Anda menjelaskan mengapa itu tidak diubah? Bagaimana saya bisa menyimpan gema test1fungsi retdan memodifikasi variabel global juga?

harrison4
sumber
Apakah Anda perlu membalas salam? Anda bisa menggemakan $ e untuk mengembalikannya. Atau gema semua yang Anda inginkan dan kemudian parse hasilnya?

Jawaban:

98

Saat Anda menggunakan substitusi perintah (yaitu $(...)konstruksi), Anda membuat subkulit. Subkulit mewarisi variabel dari cangkang induknya, tetapi ini hanya bekerja satu cara - subkulit tidak dapat mengubah lingkungan cangkang induknya. Variabel Anda edisetel di dalam subkulit, tetapi bukan shell induk. Ada dua cara untuk meneruskan nilai dari subkulit ke induknya. Pertama, Anda dapat mengeluarkan sesuatu ke stdout, lalu menangkapnya dengan substitusi perintah:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Memberikan:

Hello

Untuk nilai numerik 0-255, Anda dapat menggunakan returnuntuk meneruskan nomor sebagai status keluar:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Memberikan:

Hello - num is 4
Josh Jolly
sumber
Terima kasih untuk intinya, tetapi saya harus mengembalikan array string, dan di dalam fungsi saya harus menambahkan elemen ke dua array string global.
harrison4
3
Anda menyadari bahwa jika Anda hanya menjalankan fungsi tanpa menetapkannya ke variabel, semua variabel global di dalamnya akan diperbarui. Alih-alih mengembalikan larik string, mengapa tidak memperbarui larik string dalam fungsi lalu menetapkannya ke variabel lain setelah fungsi selesai?
@JohnDoe: Anda tidak bisa mengembalikan "larik string" dari fungsi. Yang bisa Anda lakukan hanyalah mencetak string. Namun, Anda dapat melakukan sesuatu seperti ini:setarray() { declare -ag "$1=(a b c)"; }
rici
34

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=1000juga.
  • 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 _capturehanya mengganti semua kejadian dari 3dengan 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 dke 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 xsini 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 xsaja, 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 -nyang tidak bekerja pada kesamaan bash3.x)

Hindari berubah d()

Solusi terakhir memiliki beberapa kekurangan besar:

  • d() perlu diubah
  • Ini perlu menggunakan beberapa detail internal xcaptureuntuk 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, evalAnda dapat melihat, bahwa kami memiliki kontrol 100% di lokasi ini. "Inside" the evalwe 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 xsedang berjalan.
    • Anda dapat melihatnya sendiri jika menambahkan sleep 1;sebelum printfatau _passback. _xcapture a d; echolalu keluaran xatau apertama, masing-masing.
  • The _passback xtidak 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 _xcapturesama 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 printfditutup 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 printfterakhir 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 printftidak bisa terjadi sebelum _passback, berapa lama pun _passbackwaktunya.
  • Perhatikan bahwa printfperintah tidak dijalankan sebelum baris perintah lengkap dipasang, jadi kita tidak dapat melihat artefak dari printf, secara independen bagaimana printfdiimplementasikan.

Oleh karena itu pertama-tama _passbackdieksekusi, 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

_capturemengandung 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 fndikembalikan. 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 bash4.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.
Tino
sumber
2
Anda luar biasa
Eliran Malka
14

Mungkin Anda dapat menggunakan file, menulis ke file di dalam fungsi, membaca dari file setelahnya. Saya telah berubah emenjadi array. Dalam contoh ini, kosong digunakan sebagai pemisah saat membaca kembali array.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Keluaran:

hi
first second third
first
second
third
Ashkan
sumber
13

Apa yang Anda lakukan, Anda menjalankan test1

$(test1)

di sub-shell (anak shell) dan anak shell tidak bisa memodifikasi apa pun di induk .

Anda dapat menemukannya di manual bash

Silakan Periksa: Hal-hal menghasilkan subkulit di sini

PradyJord
sumber
7

Saya memiliki masalah yang sama, ketika saya ingin secara otomatis menghapus file temp yang telah saya buat. Solusi yang saya dapatkan bukanlah menggunakan substitusi perintah, melainkan meneruskan nama variabel, yang seharusnya mengambil hasil akhir, ke dalam fungsi. Misalnya

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

Jadi, dalam kasus Anda itu adalah:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Bekerja dan tidak memiliki batasan pada "nilai kembali".

Elmar Zander
sumber
1

Itu karena substitusi perintah dilakukan di subkulit, jadi sementara subkulit mewarisi variabel, perubahan pada mereka hilang ketika subkulit berakhir.

Referensi :

Substitusi perintah, perintah yang dikelompokkan dengan tanda kurung, dan perintah asynchronous dipanggil di lingkungan subkulit yang merupakan duplikat dari lingkungan shell

Beberapa programmer
sumber
@JohnDoe Saya tidak yakin itu mungkin. Anda mungkin harus memikirkan kembali desain skrip Anda.
Beberapa programmer dude
Oh, tapi saya perlu menguji array global dalam suatu fungsi, jika tidak, saya harus mengulangi banyak kode (ulangi kode fungsi -30 baris- 15 kali -satu per panggilan-). Tidak ada cara lain, bukan?
harrison4
1

Solusi untuk masalah ini, tanpa harus memperkenalkan fungsi yang kompleks dan sangat memodifikasi fungsi aslinya, adalah dengan menyimpan nilai dalam file sementara dan membaca / menulisnya jika diperlukan.

Pendekatan ini sangat membantu saya ketika saya harus mengejek fungsi bash yang dipanggil beberapa kali dalam kasus uji kelelawar.

Misalnya, Anda dapat memiliki:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

Kekurangannya adalah Anda mungkin memerlukan beberapa file temporer untuk variabel yang berbeda. Dan juga Anda mungkin perlu mengeluarkan syncperintah untuk mempertahankan konten di disk antara satu operasi tulis dan baca.

Fabio
sumber
-1

Anda selalu dapat menggunakan alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Dino Dini
sumber