Серіалізувати змінну оболонки в bash або zsh


12

Чи є спосіб серіалізувати змінну оболонки? Припустимо, у мене є змінна $VAR, і я хочу мати можливість зберегти її у файл чи будь-що інше, а потім прочитати його пізніше, щоб отримати те саме значення назад?

Чи є портативний спосіб зробити це? (Я не думаю, що так)

Чи є спосіб це зробити в bash або zsh?


2
Увага: версія моєї відповіді, яку ви прийняли днями, мала серйозну проблему, яка порушиться в деяких сценаріях. Я переписав його, щоб включити виправлення (і додати функції), і вам слід справді перечитати його з нуля та перенести свій код для використання фіксованої версії.
Калеб

^ Ще один приклад стійкого громадянства @ Калеба.
mikeserv

Відповіді:


14

Попередження: За допомогою будь-якого з цих рішень вам слід пам’ятати, що ви довіряєте цілісності файлів даних, щоб бути безпечними, оскільки вони виконуватимуться як код оболонки у вашому сценарії. Забезпечення їх є першорядним для безпеки вашого сценарію!

Проста вбудована реалізація для серіалізації однієї або декількох змінних

Так, і в bash і zsh ви можете серіалізувати вміст змінної способом, який легко отримати за допомогою typesetвбудованого та -pаргументу. Формат виводу такий, що ви можете просто sourceотримати результат, щоб повернути свої речі.

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

Ви можете повернути такі речі як пізніше у своєму сценарії, так і в іншому сценарії:

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

Це буде працювати для bash, zsh та ksh, включаючи передачу даних між різними оболонками. Bash переведе це на свою вбудовану declareфункцію, тоді як zsh реалізує це за допомогою, typesetале оскільки bash має псевдонім для цього, так чи інакше, ми використовуємо typesetтут для ksh сумісності.

Більш складна узагальнена реалізація з використанням функцій

Вищеописана реалізація дуже проста, але якщо ви часто її зателефонуєте, вам, можливо, захочеться надати собі корисну функцію, щоб зробити її легшою. Крім того, якщо ви коли-небудь спробуєте включити вищезгадане у спеціальні функції, у вас виникнуть проблеми зі змінним масштабом. Ця версія повинна усунути ці проблеми.

Зверніть увагу на все це, щоб підтримувати перехресну сумісність bash / zsh, ми будемо виправляти і випадки, typesetі declareкод повинен працювати в одній або обох оболонках. Це додає певної маси та безладу, які можна було б усунути, якби ви робили це лише для тієї чи іншої оболонки.

Основна проблема використання функцій для цього (або включення коду в інші функції) полягає в тому, що typesetфункція генерує код, який при поверненні до сценарію всередині функції за замовчуванням створює локальну змінну, а не глобальну.

Це можна виправити за допомогою одного з декількох хаків. Моя початкова спроба виправити це було проаналізувати вихід процесу серіалізації через sedдодавання -gпрапора, щоб створений код визначав глобальну змінну при поверненні назад у.

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

Зауважте, що sedвиразний вираз повинен відповідати лише першому виникненню або 'typeset', або 'оголосити', і додати -gяк перший аргумент. Потрібно лише узгоджувати перше виникнення, оскільки, як правильно вказував у коментарях Стефан Шазелас , інакше воно також відповідатиме випадкам, коли серіалізований рядок містить буквальні нові рядки з подальшим словом оголошення або набір тексту.

У доповненні до виправлення моєї початкового розбору безтактності , Stéphane також запропонував менш крихкий спосіб зламати це , що не тільки підніжку питань з розбором рядків , але може бути корисним гачком , щоб додати додаткову функціональність, використовуючи функцію - обгортки , щоб переглянути дії прийняті під час отримання даних. Це передбачає, що ви не граєте в інші ігри з командами декларування або набору, але цей прийом було б легше реалізувати в ситуації, коли ви включали цю функціональність як частину іншої власної або ви не контролювали дані, що записуються, та -gдодавали чи ні прапор. Щось подібне можна було б зробити і з псевдонімами, див . Відповідь Гілла для реалізації.

Щоб зробити результат ще кориснішим, ми можемо перебирати кілька змінних, переданих нашим функціям, припускаючи, що кожне слово в масиві аргументів є ім'ям змінної. Результат стає приблизно таким:

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
}

З будь-яким рішенням використання виглядатиме так:

# 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"

declareє bashеквівалентом ksh's typeset. bash, zshтакож підтримка typesetв цьому плані typesetє більш портативною. export -pє POSIX, але він не приймає жодних аргументів, і його вихід залежить від оболонки (хоча це точно вказано для оболонок POSIX, так, наприклад, коли bash або ksh називаються як sh). Не забудьте цитувати ваші змінні; використовувати тут оператор split + glob не має сенсу.
Стефан Шазелас

Зверніть увагу, що він -Eє лише в деяких BSD sed. Змінні значення можуть містити символи нового рядка, тому sed 's/^.../.../'не гарантується, що вони працюватимуть правильно.
Stéphane Chazelas

Це саме те, що я шукав! Мені хотілося зручного способу переміщення змінних туди-сюди між оболонками.
fwenom

Я мав на увазі: a=$'foo\ndeclare bar' bash -c 'declare -p a'для встановлення буде виводиться рядок, що починається з declare. Це, мабуть, краще зробити declare() { builtin declare -g "$@"; }перед тим, як зателефонувати source(і зняти з нього згодом)
Stéphane Chazelas

2
@Gilles, псевдоніми не працюватимуть усередині функцій (їх потрібно визначити під час визначення функції), а з bash це означатиме, що вам потрібно буде робити shopt -s expandaliasте, що не є інтерактивним. За допомогою функцій ви також можете покращити declareобгортку, щоб вона відновила лише вказані вами змінні.
Стефан Шазелас

3

Використовуйте перенаправлення, заміну команд та розширення параметрів. Подвійні лапки потрібні для збереження пробілів та спеціальних символів. Трейлінг xзберігає нові рядки, які були б інакше видалені при заміні команд.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}

Ймовірно, він хоче зберегти ім'я змінної також у файл.
користувач80551

2

Серіалізувати всі - POSIX

У будь-якій оболонці POSIX ви можете серіалізувати всі змінні середовища export -p. Це не включає неекспортні змінні оболонки. Вихід правильно цитується, щоб ви могли прочитати його назад в тій же оболонці і отримати точно однакові змінні значення. Вихід може бути не читабельним в іншій оболонці, наприклад, ksh використовує $'…'синтаксис non POSIX .

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

Серіалізуйте деякі чи всі - ksh, bash, zsh

Кш (і pdksh / mksh, і ATT ksh), bash і zsh забезпечують кращий об'єкт із typesetвбудованим. typeset -pвиводить усі визначені змінні та їх значення (zsh опускає значення змінних, які були приховані typeset -H). Вихід містить належне оголошення, так що змінні середовища експортуються при зчитуванні назад (але якщо змінна вже експортується при зчитуванні назад, вона не буде експортованою), так що масиви будуть зчитуватися як масиви тощо. Тут також вихід належним чином цитується, але гарантовано читається лише в одній оболонці. Ви можете передавати набір змінних для серіалізації в командному рядку; якщо ви не передаєте будь-яку змінну, всі вони серіалізуються.

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

У bash та zsh відновлення неможливо виконати з функції, оскільки typesetоператори всередині функції відносяться до цієї функції. Вам потрібно запуститись . ./some_varsу контексті, де ви хочете використовувати значення змінних, дбаючи про те, щоб змінні, які були експортованими глобальними, були передекларовані як глобальні. Якщо ви хочете прочитати значення в межах функції та експортувати їх, ви можете оголосити тимчасовий псевдонім або функцію. В zsh:

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

У bash (який використовує, declareа не typeset):

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

У ksh typesetоголошує локальні змінні у функціях, визначених з, function function_name { … }та глобальні змінні у функціях, визначених за допомогою function_name () { … }.

Серіалізувати деякі - POSIX

Якщо ви хочете більше контролю, ви можете експортувати вміст змінної вручну. Щоб надрукувати вміст змінної саме у файл, використовуйте printfвбудований ( echoмає кілька спеціальних випадків, наприклад, echo -nна деяких оболонках та додає новий рядок):

printf %s "$VAR" >VAR.content

Ви можете прочитати це назад за допомогою $(cat VAR.content), за винятком того, що заміна команди позбавляє останніх рядків. Щоб уникнути цієї зморшки, домовтеся, щоб результат ніколи не закінчувався новим рядком.

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

Якщо ви хочете надрукувати кілька змінних, ви можете процитувати їх за допомогою одиничних лапок і замінити всі вбудовані одиничні лапки на '\''. Цю форму цитування можна прочитати назад у будь-якій оболонці стилю Bourne / POSIX. Наступний фрагмент працює в будь-якій оболонці POSIX. Він працює лише для змінних рядків (і числових змінних в оболонках, які їх містять, хоча вони будуть читатись як рядки), він не намагається мати справу зі змінними масиву в оболонках, які їх мають.

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
}

Ось ще один підхід, який не розщеплює підпроцес, але важчий при маніпулюванні рядками.

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
}

Зауважте, що на оболонках, які дозволяють змінні лише для читання, ви отримаєте помилку, якщо спробуєте прочитати назад змінну, яка є лише для читання.


Це призводить до змінних, таких як $PWDі $_- будь ласка, дивіться свої власні коментарі нижче.
mikeserv

@Caleb Як щодо створення typesetпсевдоніма для typeset -g?
Жил "ТАК - перестань бути злим"

@Gilles Я подумав про це після того, як Стефанія запропонувала метод функції, але я не знала, як переносимо встановити необхідні параметри розширення псевдоніму по оболонках. Можливо, ви могли б поставити це у своїй відповіді як життєздатну альтернативу функції, яку я включив.
Калеб

0

Велике спасибі @ stéphane-chazelas, який вказав на всі проблеми з моїми попередніми спробами, тепер, здається, працює над тим, щоб серіалізувати масив для stdout або в змінну.

Ця методика не аналізує введення оболонки (на відміну від declare -a/ declare -p) і тому є захищеною від зловмисного вставки метахарактерів у серіалізований текст.

Примітка. Нові рядки не уникаються, тому що readвидаляється \<newlines>пара символів, тому -d ...замість цього потрібно передати їх для читання, а потім нерозроблені нові рядки зберегти.

Все це управляється у unserialiseфункції.

Використовуються два магічні символи, роздільник поля та роздільник записів (так що декілька масивів можна серіалізувати в один і той же потік).

Ці символи не можуть бути визначені як FSі , RSале і не можуть бути визначені як newlineсимвол , так як втекла новий рядок видалена read.

Символ втечі повинен бути \зворотним косою рисою, оскільки саме це використовується, readщоб уникнути розпізнавання IFSсимволу як символу.

serialiseбуде серіалізувати "$@"до stdout, serialise_toсеріалізуватиметься на присвійну назву в$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
}

і несеріалізувати з:

unserialise data # read from stdin

або

unserialise data "$serialised_data" # from args

напр

$ 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:

(без зворотного нового рядка)

прочитати його назад:

$ 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

або

unserialise array # read from stdin

Bash readповажає символ втечі \(якщо ви не передаєте прапор -r), щоб видалити спеціальне значення символів, наприклад, для поділу поля введення чи обмеження рядка.

Якщо ви хочете серіалізувати масив замість простого списку аргументів, просто передайте масив як список аргументів:

serialise_array "${my_array[@]}"

Ви можете використовувати unserialiseв циклі, як ви хочете, readтому що це просто обгорнуте зчитування - але пам’ятайте, що потік не розділений новим рядком:

while unserialise array
do ...
done

Він не працює, якщо елементи містять недруковані (у поточній мові) або керуючі символи, такі як TAB або новий рядок, як тоді, bashі zshнадають їх як $'\xxx'. Спробуйте з bash -c $'printf "%q\n" "\t"'абоbash -c $'printf "%q\n" "\u0378"'
Stéphane Chazelas

чорт тотін, ти маєш рацію! Я зміню свою відповідь, щоб не використовувати printf% q, але $ {@ // .. / ..} ітерацій, щоб уникнути білого простору,
Сем Ліддікотт,

Це рішення залежить від $IFSнемодифікованості, і тепер не вдається належним чином відновити порожні елементи масиву. Насправді, було б більше сенсу використовувати інше значення IFS та використовувати, -d ''щоб уникнути необхідності виходити з нового рядка. Наприклад, використовуйте :як роздільник поля, а лише уникайте цього та звороту косу рису та використовуйте IFS=: read -ad '' arrayдля імпорту.
Стефан Шазелас

Так .... Я забув про спеціальну обробку, що руйнується пробілом, коли використовується як роздільник поля в режимі зчитування. Я радий, що ти сьогодні на балі! Ви маєте рацію про -d "", щоб уникнути \ n, але в моєму випадку я хотів прочитати потік серіалізацій - я все-таки адаптую відповідь. Дякую!
Сем Ліддікотт

Утеча від нового рядка не дозволяє зберегти його, він змушує його один раз піти read. backslash-newline для read- це спосіб продовжити логічну лінію на іншу фізичну лінію. Редагувати: ах, я бачу, ви вже згадали про проблему з новим рядком.
Стефан Шазелас

0

Ви можете використовувати base64:

$ VAR="1/ 
,x"
$ echo "$VAR" | base64 > f
$ VAR=$(cat f | base64 -d)
$ echo "${VAR}X"
1/ 
,xX

-2
printf 'VAR=$(cat <<\'$$VAR$$'\n%s\n'$$VAR$$'\n)' "$VAR" >./VAR.file

Ще один спосіб зробити це - переконатися, що ви обробляєте всі 'такі тверді цитати:

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

Або з export:

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

Перший та другий параметри працюють у будь-якій оболонці POSIX, припускаючи, що значення змінної не містить рядка:

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

Третій варіант повинен працювати для будь-якої оболонки POSIX, але може намагатися визначити інші змінні, такі як _або PWD. Однак правда полягає в тому, що єдині змінні, які він може спробувати визначити, встановлюються і підтримуються самою оболонкою - і тому навіть якщо ви імпортуєте exportзначення для будь-якого з них - $PWDнаприклад, оболонка просто скине їх на правильне значення негайно в будь-якому випадку - спробуйте зробити PWD=any_valueі переконатися в цьому самі.

І оскільки - принаймні з GNU bash- вихід налагодження автоматично безпечно котирується для повторного введення в оболонку, це працює незалежно від кількості 'жорстких цитат у "$VAR":

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

$VAR пізніше можна встановити збережене значення в будь-якому сценарії, у якому наступний шлях дійсний з:

. ./VAR.file

Я не впевнений, що ви намагалися написати в першій команді. $$це ПІД запущеної оболонки, ви помилилися з цитуванням і мали на увазі \$чи щось таке? Основний підхід використання документа тут можна зробити так, щоб він працював, але це складний, а не однолінійний матеріал: що б ви не вибрали як кінцевий маркер, ви повинні вибрати те, що не відображається в рядку.
Жил "ТАК - перестань бути злим"

Друга команда не працює, коли $VARмістить %. Третя команда не завжди працює зі значеннями, що містять кілька рядків (навіть після додавання очевидно відсутніх подвійних лапок).
Жил 'ТАК - перестань бути злим'

@Gilles - я знаю його pid - я використовував його як просте джерело встановлення унікального роздільника. Що ви маєте на увазі під "не завжди" точно? І я не розумію, яких подвійних лапок не вистачає - усі вони є змінними призначеннями. Подвійні цитати лише плутають ситуацію в цьому контексті.
mikeserv

@Gilles - Я відкликаю справу про призначення, - це аргумент env. Мені все ще цікаво, що ви маєте на увазі про кілька рядків - sedвидаляє кожен рядок до зустрічі VAR=до останнього, - тому всі рядки передачі $VARпередаються далі. Чи можете ви надати приклад, який його порушує?
mikeserv

Ах, вибачте, третій метод працює (з виправленням цитування). Ну, припускаючи , що ім'я змінної (тут VAR) не змінюється PWDабо _або , можливо , інші , що деякі оболонки визначають. Другий метод вимагає баш; формат виводу з -vне стандартизований (жоден тире, ksh93, mksh і zsh не працює).
Жиль 'ТАК - перестань бути злим'

-2

Майже так само, але трохи інакше:

З вашого сценарію:

#!/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"

Цього разу випробовано вище.


Я бачу, ти не тестував! Основна логіка працює, але це не важкий біт. Важкий біт - це цитувати речі належним чином, і ви нічого з цього не робите. Спробуйте змінні, значення яких містять символи нового рядка, ', *і т.д.
Жиля SO- перестати бути злом "

echo "$LVALUE=\"$RVALUE\""повинен зберігати нові рядки, і результат у cfg_file повинен бути таким: MY_VAR1 = "Line1 \ nLine 2" Таким чином, коли eval MY_VAR1, він буде містити і нові рядки. Звичайно, у вас можуть виникнути проблеми, якщо ваше збережене значення містить себе "char. Але і про це можна було б подбати.
vadimbog

1
До речі, навіщо проголосувати за те, що правильно відповідає на поставлене тут питання? Вище працює для мене дуже добре і використовую скрізь у своїх сценаріях?
vadimbog
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.