Serialize variabel shell dalam bash atau zsh

12

Apakah ada cara untuk membuat serial sebuah variabel shell? Misalkan saya memiliki variabel $VAR, dan saya ingin dapat menyimpannya ke file atau apa pun, dan kemudian membacanya kembali nanti untuk mendapatkan nilai yang sama kembali?

Apakah ada cara portabel untuk melakukan ini? (Kurasa tidak)

Apakah ada cara untuk melakukannya di bash atau zsh?

racun
sumber
2
Perhatian: versi jawaban saya yang Anda terima kemarin memiliki masalah serius yang akan pecah dalam beberapa skenario. Saya telah menulis ulang untuk menyertakan perbaikan (dan menambahkan fitur) dan Anda harus benar-benar membacanya dari awal dan port kode Anda untuk menggunakan versi tetap.
Caleb
^ Contoh lain ^ kewarganegaraan @ Caleb yang terhormat.
mikeserv

Jawaban:

14

Peringatan: Dengan salah satu solusi ini, Anda harus menyadari bahwa Anda memercayai integritas file data agar aman karena akan dieksekusi sebagai kode shell dalam skrip Anda. Mengamankan mereka sangat penting untuk keamanan skrip Anda!

Implementasi inline sederhana untuk membuat serial satu atau lebih variabel

Ya, dalam bash dan zsh Anda bisa membuat serial konten dari suatu variabel dengan cara yang mudah diambil menggunakan typesetbuiltin dan -pargumen. Format output sedemikian rupa sehingga Anda dapat dengan mudah sourceoutput untuk mendapatkan barang-barang Anda kembali.

 # You have variable(s) $FOO and $BAR already with your stuff
 typeset -p FOO BAR > ./serialized_data.sh

Anda bisa mendapatkan barang-barang Anda kembali seperti ini nanti di skrip Anda atau di skrip lain sekaligus:

# Load up the serialized data back into the current shell
source serialized_data.sh

Ini akan bekerja untuk bash, zsh dan ksh termasuk mengirimkan data antara shell yang berbeda. Bash akan menerjemahkan ini ke declarefungsi bawaannya sementara zsh mengimplementasikan ini dengan typesettetapi sebagai bash memiliki alias untuk ini berfungsi baik cara yang kami gunakan di typesetsini untuk kompatibilitas ksh.

Implementasi umum yang lebih kompleks menggunakan fungsi

Implementasi di atas sangat sederhana, tetapi jika Anda sering memanggilnya, Anda mungkin ingin memberi diri Anda fungsi utilitas untuk membuatnya lebih mudah. Selain itu jika Anda pernah mencoba untuk memasukkan fungsi-fungsi khusus di dalam Anda akan mengalami masalah dengan pelingkupan variabel. Versi ini harus menghilangkan masalah-masalah itu.

Catatan untuk semua ini, untuk menjaga kompatibilitas silang bash / zsh, kami akan memperbaiki kedua kasus typesetdan declaresehingga kode harus bekerja di salah satu atau kedua shell. Ini menambahkan sejumlah besar dan kekacauan yang bisa dihilangkan jika Anda hanya melakukan ini untuk satu shell atau yang lain.

Masalah utama dengan menggunakan fungsi untuk ini (atau termasuk kode dalam fungsi lain) adalah bahwa typesetfungsi menghasilkan kode itu, ketika bersumber kembali ke dalam skrip dari dalam suatu fungsi, default untuk membuat variabel lokal daripada yang global.

Ini dapat diperbaiki dengan satu dari beberapa peretasan. Upaya awal saya untuk memperbaikinya adalah mem-parsing output dari proses serialisasi sedhingga menambahkan -gflag sehingga kode yang dibuat mendefinisikan variabel global ketika bersumber kembali.

serialize() {
    typeset -p "$1" | sed -E '0,/^(typeset|declare)/{s/ / -g /}' > "./serialized_$1.sh"
}
deserialize() {
    source "./serialized_$1.sh"
}

Perhatikan bahwa sedekspresi funky hanya untuk mencocokkan dengan kemunculan pertama 'typeset' atau 'menyatakan' dan menambahkan -gsebagai argumen pertama. Hal ini diperlukan untuk hanya mencocokkan kejadian pertama karena, seperti Stéphane Chazelas menunjukkan dengan benar dalam komentar, jika tidak, itu juga akan cocok dengan kasus di mana string serial berisi baris baru literal diikuti dengan kata menyatakan atau mengeset.

Selain mengoreksi awal parsing saya kecerobohan , Stéphane juga menyarankan cara yang kurang rapuh untuk hack ini yang tidak hanya langkah samping masalah dengan parsing string tetapi bisa menjadi hook berguna untuk menambah fungsionalitas tambahan dengan menggunakan fungsi pembungkus untuk mendefinisikan tindakan diambil saat sumber data kembali masuk. Ini mengasumsikan Anda tidak memainkan game lain dengan perintah mendeklarasikan atau mengeset, tetapi teknik ini akan lebih mudah untuk diterapkan dalam situasi di mana Anda memasukkan fungsi ini sebagai bagian dari fungsi lain dari Anda sendiri atau Anda tidak mengendalikan data yang sedang ditulis dan apakah -gbendera itu ditambahkan atau tidak . Hal serupa juga bisa dilakukan dengan alias, lihat jawaban Gilles untuk implementasi.

Untuk menjadikan hasilnya lebih bermanfaat, kita dapat mengulangi beberapa variabel yang diteruskan ke fungsi kita dengan mengasumsikan bahwa setiap kata dalam array argumen adalah nama variabel. Hasilnya menjadi seperti ini:

serialize() {
    for var in $@; do
        typeset -p "$var" > "./serialized_$var.sh"
    done
}

deserialize() {
    declare() { builtin declare -g "$@"; }
    typeset() { builtin typeset -g "$@"; }
    for var in $@; do
        source "./serialized_$var.sh"
    done
    unset -f declare typeset
}

Dengan salah satu solusi, penggunaan akan terlihat seperti ini:

# Load some test data into variables
FOO=(an array or something)
BAR=$(uptime)

# Save it out to our serialized data files
serialize FOO BAR

# For testing purposes unset the variables to we know if it worked
unset FOO BAR

# Load  the data back in from out data files
deserialize FOO BAR

echo "FOO: $FOO\nBAR: $BAR"
Caleb
sumber
declareadalah bashsetara kshdengan typeset. bash, zshjuga mendukung typesetsehingga dalam hal itu, typesetlebih portabel. export -padalah POSIX, tetapi ia tidak mengambil argumen apa pun dan hasilnya tergantung pada shell (meskipun itu ditentukan dengan baik untuk shell POSIX, jadi misalnya ketika bash atau ksh disebut sebagai sh). Ingatlah untuk mengutip variabel Anda; menggunakan operator split + glob di sini tidak masuk akal.
Stéphane Chazelas
Catatan yang -Ehanya ditemukan di beberapa BSD sed. Nilai variabel dapat berisi karakter baris baru, sehingga sed 's/^.../.../'tidak dijamin berfungsi dengan benar.
Stéphane Chazelas
Ini persis apa yang saya cari! Saya ingin cara mudah untuk mendorong variabel bolak-balik di antara shell.
fwenom
Maksud saya: a=$'foo\ndeclare bar' bash -c 'declare -p a'untuk menginstal akan menampilkan garis yang dimulai dengan declare. Mungkin lebih baik dilakukan declare() { builtin declare -g "$@"; }sebelum menelepon source(dan tidak
mengaturnya
2
@Gilles, alias tidak akan berfungsi di dalam fungsi-fungsi (perlu didefinisikan pada saat definisi fungsi), dan dengan bash itu berarti Anda harus melakukan shopt -s expandaliasketika tidak interaktif. Dengan fungsi, Anda juga bisa meningkatkan declarepembungkus sehingga hanya mengembalikan variabel yang Anda tentukan.
Stéphane Chazelas
3

Gunakan pengalihan, substitusi perintah, dan ekspansi parameter. Kutipan ganda diperlukan untuk mempertahankan spasi dan karakter khusus. Trailing xmenyimpan baris baru trailing yang akan dihapus dalam substitusi perintah.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}
choroba
sumber
Dia mungkin ingin menyimpan nama variabel juga ke file.
user80551
2

Serialisasi semua - POSIX

Di setiap shell POSIX, Anda bisa membuat serial semua variabel lingkungan dengan export -p. Ini tidak termasuk variabel shell yang tidak diekspor. Outputnya dikutip dengan benar sehingga Anda dapat membacanya kembali di shell yang sama dan mendapatkan nilai variabel yang persis sama. Output mungkin tidak dapat dibaca di shell lain, misalnya ksh menggunakan $'…'sintaks non-POSIX .

save_environment () {
  export -p >my_environment
}
restore_environment () {
  . ./my_environment
}

Serialisasi beberapa atau semua - ksh, bash, zsh

Ksh (baik pdksh / mksh dan ATT ksh), bash dan zsh menyediakan fasilitas yang lebih baik dengan typesetbuiltin. typeset -pmencetak semua variabel yang ditentukan dan nilainya (zsh menghilangkan nilai variabel yang telah disembunyikan typeset -H). Outputnya berisi deklarasi yang tepat sehingga variabel lingkungan diekspor saat dibaca kembali (tetapi jika variabel sudah diekspor saat dibaca kembali, itu tidak akan diekspor), sehingga array dibaca kembali sebagai array, dll. Di sini juga, output dikutip dengan benar tetapi hanya dijamin dapat dibaca dalam shell yang sama. Anda dapat melewati serangkaian variabel untuk membuat serial pada baris perintah; Jika Anda tidak lulus variabel apa pun maka semua serial.

save_some_variables () {
  typeset -p VAR OTHER_VAR >some_vars
}

Dalam bash dan zsh, mengembalikan tidak dapat dilakukan dari suatu fungsi karena typesetpernyataan di dalam suatu fungsi dibatasi untuk fungsi itu. Anda perlu menjalankan . ./some_varsdalam konteks di mana Anda ingin menggunakan nilai-nilai variabel, berhati-hatilah bahwa variabel yang global ketika diekspor akan dideklarasikan ulang sebagai global. Jika Anda ingin membaca kembali nilai-nilai dalam suatu fungsi dan mengekspornya, Anda dapat mendeklarasikan alias atau fungsi sementara. Dalam zsh:

restore_and_make_all_global () {
  alias typeset='typeset -g'
  . ./some_vars
  unalias typeset
}

Di bash (yang menggunakan declare daripada typeset):

restore_and_make_all_global () {
  alias declare='declare -g'
  shopt -s expand_aliases
  . ./some_vars
  unalias declare
}

Di ksh, typeset deklarasikan variabel lokal dalam fungsi yang didefinisikan function function_name { … }dan variabel global dalam fungsi yang didefinisikan function_name () { … }.

Serialize some - POSIX

Jika Anda ingin lebih banyak kontrol, Anda dapat mengekspor konten variabel secara manual. Untuk mencetak konten variabel secara tepat ke dalam file, gunakan printfbuiltin ( echomemiliki beberapa kasus khusus sepertiecho -n pada beberapa shell dan menambahkan baris baru):

printf %s "$VAR" >VAR.content

Anda dapat membaca ini kembali dengan $(cat VAR.content), kecuali bahwa substitusi perintah menanggalkan baris baru. Untuk menghindari kerutan ini, atur agar keluaran tidak berakhir dengan baris baru.

VAR=$(cat VAR.content && echo a)
if [ $? -ne 0 ]; then echo 1>&2 "Error reading back VAR"; exit 2; fi
VAR=${VAR%?}

Jika Anda ingin mencetak beberapa variabel, Anda dapat mengutipnya dengan tanda kutip tunggal, dan mengganti semua tanda kutip tunggal yang disematkan '\''. Bentuk kutipan ini dapat dibaca kembali ke shell gaya Bourne / POSIX apa pun. Cuplikan berikut ini berfungsi di shell POSIX apa pun. Ini hanya berfungsi untuk variabel string (dan variabel numerik di shell yang memilikinya, meskipun mereka akan dibaca kembali sebagai string), itu tidak mencoba untuk berurusan dengan variabel array di shell yang memilikinya.

serialize_variables () {
  for __serialize_variables_x do
    eval "printf $__serialize_variables_x=\\'%s\\'\\\\n \"\$${__serialize_variables_x}\"" |
    sed -e "s/'/'\\\\''/g" -e '1 s/=.../=/' -e '$ s/...$//'
  done
}

Berikut ini pendekatan lain yang tidak membayar subproses tetapi lebih berat pada manipulasi string.

serialize_variables () {
  for __serialize_variables_var do
    eval "__serialize_variables_tail=\${$__serialize_variables_var}"
    while __serialize_variables_quoted="$__serialize_variables_quoted${__serialize_variables_tail%%\'*}"
          [ "${__serialize_variables_tail%%\'*}" != "$__serialize_variables_tail" ]; do
      __serialize_variables_tail="${__serialize_variables_tail#*\'}"
      __serialize_variables_quoted="${__serialize_variables_quoted}'\\''"
    done
    printf "$__serialize_variables_var='%s'\n" "$__serialize_variables_quoted"
  done
}

Perhatikan bahwa pada shell yang memungkinkan variabel read-only, Anda akan mendapatkan kesalahan jika Anda mencoba membaca kembali variabel yang read-only.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Ini membawa variabel seperti $PWDdan $_- silakan lihat komentar Anda sendiri di bawah ini.
mikeserv
@ Caleb Bagaimana kalau membuat typesetalias untuk typeset -g?
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles Saya memikirkan hal itu setelah Stephanie menyarankan metode fungsi, tetapi saya tidak yakin bagaimana mengatur portable yang diperlukan alias perluas opsi ekspansi lintas shell. Mungkin Anda bisa menempatkan itu dalam jawaban Anda sebagai alternatif yang layak untuk fungsi yang saya sertakan.
Caleb
0

Banyak terima kasih kepada @ stéphane-chazelas yang menunjukkan semua masalah dengan upaya saya sebelumnya, sekarang ini tampaknya bekerja untuk serialise array ke stdout atau ke dalam variabel.

Teknik ini tidak mem-parsing input (tidak seperti declare -a / declare -p) dan aman terhadap penyisipan metakarakter berbahaya dalam teks serial.

Catatan: baris baru tidak luput, karena readmenghapus\<newlines> pasangan karakter, jadi -d ...alih-alih harus diteruskan untuk membaca, dan kemudian baris dipertahankan.

Semua ini dikelola di unserialise fungsinya.

Dua karakter ajaib digunakan, pemisah bidang dan pemisah rekaman (sehingga beberapa array dapat diserialisasi ke aliran yang sama).

Karakter-karakter ini dapat didefinisikan sebagai FSdan RStetapi tidak dapat didefinisikan sebagai newlinekarakter karena baris baru yang dihapus dihapus olehread .

Karakter pelarian haruslah \garis miring terbalik, karena itulah yang digunakan oleh readuntuk menghindari karakter yang dikenali sebagai IFSkarakter.

serialiseakan bersambung "$@"ke stdout, serialise_toakan bersambung ke varable yang bernama in$1

serialise() {
  set -- "${@//\\/\\\\}" # \
  set -- "${@//${FS:-;}/\\${FS:-;}}" # ; - our field separator
  set -- "${@//${RS:-:}/\\${RS:-:}}" # ; - our record separator
  local IFS="${FS:-;}"
  printf ${SERIALIZE_TARGET:+-v"$SERIALIZE_TARGET"} "%s" "$*${RS:-:}"
}
serialise_to() {
  SERIALIZE_TARGET="$1" serialise "${@:2}"
}
unserialise() {
  local IFS="${FS:-;}"
  if test -n "$2"
  then read -d "${RS:-:}" -a "$1" <<<"${*:2}"
  else read -d "${RS:-:}" -a "$1"
  fi
}

dan tidak terhubung dengan:

unserialise data # read from stdin

atau

unserialise data "$serialised_data" # from args

misalnya

$ serialise "Now is the time" "For all good men" "To drink \$drink" "At the \`party\`" $'Party\tParty\tParty'
Now is the time;For all good men;To drink $drink;At the `party`;Party   Party   Party:

(tanpa baris baru)

baca kembali:

$ serialise_to s "Now is the time" "For all good men" "To drink \$drink" "At the \`party\`" $'Party\tParty\tParty'
$ unserialise array "$s"
$ echo "${array[@]/#/$'\n'}"

Now is the time 
For all good men 
To drink $drink 
At the `party` 
Party   Party   Party

atau

unserialise array # read from stdin

Bash readmenghormati karakter pelariannya\ (kecuali Anda melewati flag -r) untuk menghapus makna khusus karakter seperti untuk pemisahan bidang input atau pembatas garis.

Jika Anda ingin membuat serial array bukan hanya daftar argumen, maka hanya lulus array Anda sebagai daftar argumen:

serialise_array "${my_array[@]}"

Anda dapat menggunakan unserialisedalam satu lingkaran seperti yang Anda lakukan readkarena itu hanya pembacaan terbungkus - tetapi ingat bahwa alirannya tidak dipisahkan dengan baris baru:

while unserialise array
do ...
done
Sam Liddicott
sumber
Ini tidak berfungsi jika elemen-elemennya mengandung yang tidak dapat dicetak (di lokal saat ini) atau mengontrol karakter seperti TAB atau baris baru saat itu bashdan zshmenjadikannya sebagai $'\xxx'. Coba dengan bash -c $'printf "%q\n" "\t"'ataubash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas
sialan tootin, kau benar! Saya akan mengubah jawaban saya untuk tidak menggunakan printf% q tetapi sebagai ganti $ {@ // .. /}} untuk keluar dari ruang putih sebagai gantinya
Sam Liddicott
Solusi itu tergantung pada $IFStidak dimodifikasi dan sekarang gagal mengembalikan elemen array kosong dengan benar. Bahkan, akan lebih masuk akal untuk menggunakan nilai IFS yang berbeda, dan gunakan -d ''untuk menghindari keharusan keluar dari baris baru. Sebagai contoh, gunakan :sebagai pemisah bidang dan hanya lolos itu dan backslash dan gunakan IFS=: read -ad '' arrayuntuk mengimpor.
Stéphane Chazelas
Yup .... Saya lupa tentang white-space collapsing perlakuan khusus ketika digunakan sebagai pemisah bidang di baca. Saya senang Anda berada di pesta dansa hari ini! Anda benar tentang -d "" untuk menghindari melarikan diri \ n, tetapi dalam kasus saya, saya ingin membaca aliran serialisasi - saya akan mengadaptasi jawabannya. Terima kasih!
Sam Liddicott
Melarikan diri dari baris baru tidak memungkinkannya untuk dipertahankan, itu membuatnya pergi sekali read. backslash-newline untuk readadalah cara untuk melanjutkan garis logis ke garis fisik lain. Sunting: ah saya melihat Anda menyebutkan masalah dengan baris baru sudah.
Stéphane Chazelas
0

Anda bisa menggunakan base64:

$ VAR="1/ 
,x"
$ echo "$VAR" | base64 > f
$ VAR=$(cat f | base64 -d)
$ echo "${VAR}X"
1/ 
,xX
aleb
sumber
-2
printf 'VAR=$(cat <<\'$$VAR$$'\n%s\n'$$VAR$$'\n)' "$VAR" >./VAR.file

Cara lain untuk melakukannya adalah memastikan Anda menangani semua 'hardquote seperti ini:

sed '"s/'"'/&"&"&/g;H;1h;$!d;g;'"s/.*/VAR='&'/" <<$$VAR$$ >./VAR.file
$VAR
$$VAR$$

Atau dengan export:

env - "VAR=$VAR" sh -c 'export -p' >./VAR.file 

Opsi pertama dan kedua bekerja di setiap shell POSIX, dengan asumsi bahwa nilai variabel tidak mengandung string:

"\n${CURRENT_SHELLS_PID}VAR${CURRENT_SHELLS_PID}\n" 

Opsi ketiga harus bekerja untuk setiap shell POSIX tetapi dapat mencoba untuk mendefinisikan variabel lain seperti _atau PWD. Kenyataannya adalah, bahwa satu-satunya variabel yang mungkin berusaha untuk didefinisikan ditetapkan dan dikelola oleh shell itu sendiri - dan bahkan jika Anda melakukan impor exportnilai untuk salah satu dari mereka - seperti $PWDmisalnya - shell hanya akan mengatur ulang ke tetap nilai yang benar - coba lakukanPWD=any_value dan lihat sendiri.

Dan karena - setidaknya dengan GNU's bash- debug output secara otomatis dikutip untuk re-input ke shell, ini berfungsi terlepas dari jumlah 'kutipan keras di "$VAR":

 PS4= VAR=$VAR sh -cx 'VAR=$VAR' 2>./VAR.file

$VAR nantinya dapat diatur ke nilai yang disimpan dalam skrip apa pun di mana jalur berikut ini valid dengan:

. ./VAR.file
mikeserv
sumber
Saya tidak yakin apa yang Anda coba tulis dalam perintah pertama. $$adalah PID shell yang berjalan, apakah Anda salah mengutip dan berarti \$atau sesuatu? Pendekatan dasar menggunakan dokumen di sini bisa dilakukan untuk bekerja, tetapi itu rumit, bukan materi satu-liner: apa pun yang Anda pilih sebagai penanda akhir, Anda harus memilih sesuatu yang tidak muncul dalam string.
Gilles 'SO- berhenti menjadi jahat'
Perintah kedua tidak berfungsi saat $VARberisi %. Perintah ketiga tidak selalu bekerja dengan nilai-nilai yang mengandung banyak baris (bahkan setelah menambahkan tanda kutip ganda yang jelas hilang).
Gilles 'SO- stop being evil'
@Gilles - Saya tahu ini pid - Saya menggunakannya sebagai sumber sederhana untuk menetapkan pembatas yang unik. Apa yang Anda maksud dengan "tidak selalu" tepatnya? Dan saya tidak mengerti apa tanda kutip ganda yang hilang - semua itu adalah penugasan variabel. Kutipan ganda hanya membingungkan situasi dalam konteks itu.
mikeserv
@Gilles - Saya menarik kembali tugas tugas - itu argumen untuk env. Saya masih ingin tahu apa yang Anda maksud tentang beberapa baris - sedmenghapus setiap baris sampai bertemu VAR=sampai akhir - jadi semua baris $VARditeruskan. Bisakah Anda memberikan contoh yang merusaknya?
mikeserv
Ah, permintaan maaf, metode ketiga berhasil (dengan koreksi penawaran). Nah, dengan asumsi nama variabel (di sini VAR) tidak berubah PWDatau _atau yang lain yang didefinisikan oleh beberapa shell. Metode kedua membutuhkan bash; format output dari -vtidak distandarisasi (tidak ada tanda hubung, ksh93, mksh dan zsh berfungsi).
Gilles 'SO- stop being evil'
-2

Hampir sama tetapi sedikit berbeda:

Dari skrip Anda:

#!/usr/bin/ksh 

save_var()
{

    (for ITEM in $*
    do
        LVALUE='${'${ITEM}'}'
        eval RVALUE="$LVALUE"
        echo "$ITEM=\"$RVALUE\""  
    done) >> $cfg_file
}

restore_vars()
{
    . $cfg_file
}

cfg_file=config_file
MY_VAR1="Test value 1"
MY_VAR2="Test 
value 2"

save_var MY_VAR1 MY_VAR2
MY_VAR1=""
MY_VAR2=""

restore_vars 

echo "$MY_VAR1"
echo "$MY_VAR2"

Kali ini diuji.

vadimbog
sumber
Saya dapat melihat Anda tidak menguji! Logika inti berfungsi, tetapi itu bukan bagian yang sulit. Bagian yang sulit adalah mengutip sesuatu dengan benar, dan Anda tidak melakukan semua itu. Coba variabel yang nilai mengandung baris baru, ', *, dll
Gilles 'SO berhenti menjadi jahat'
echo "$LVALUE=\"$RVALUE\""seharusnya menjaga baris baru juga dan hasil dalam cfg_file harus seperti: MY_VAR1 = "Line1 \ nLine 2" Jadi ketika eval MY_VAR1 itu akan berisi baris baru juga. Tentu saja Anda mungkin memiliki masalah jika nilai yang disimpan mengandung "char sendiri . Tapi itu bisa diurus juga.
vadimbog
1
Btw, mengapa untuk memilih sesuatu yang menjawab dengan benar pertanyaan yang diajukan di sini? Di atas berfungsi dengan baik untuk saya dan menggunakan di mana saja dalam skrip saya?
vadimbog