Сериализация переменной оболочки в bash или zsh

12

Есть ли способ сериализации переменной оболочки? Предположим, у меня есть переменная $VAR, и я хочу иметь возможность сохранить ее в файл или что-то еще, а затем прочитать позже, чтобы вернуть то же значение?

Есть ли портативный способ сделать это? (Я так не думаю)

Есть ли способ сделать это в bash или zsh?

fwenom
источник
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выражение funky должно соответствовать только первому вхождению 'typeset' или 'Declare' и добавляться -gв качестве первого аргумента. Необходимо соответствовать только первому вхождению, потому что, как справедливо указал Стефан Шазелас в комментариях, в противном случае он также будет соответствовать случаям, когда в сериализованной строке содержатся буквенные символы новой строки, за которыми следует слово Declare или typeset.

В дополнении к исправлению моей первоначальному разбора бестактности , 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 называется as sh). Не забудьте процитировать ваши переменные; использование оператора split + glob здесь не имеет смысла.
Стефан Шазелас
Обратите внимание, что -Eвстречается только в некоторых BSD sed. Значения переменных могут содержать символы новой строки, поэтому работа sed 's/^.../.../'не гарантируется.
Стефан Шазелас
Это именно то, что я искал! Я хотел удобный способ перемещать переменные вперед и назад между оболочками.
fwenom
Я имел в виду: a=$'foo\ndeclare bar' bash -c 'declare -p a'для установки выведет строку, которая начинается с declare. Вероятно, это лучше сделать declare() { builtin declare -g "$@"; }перед звонком source(и отключить после этого)
Стефан Шазелас
2
@Gilles, псевдонимы не будут работать внутри функций (должны быть определены во время определения функции), а с bash это означает, что вам нужно будет делать, shopt -s expandaliasкогда не интерактивно. С помощью функций вы также можете улучшить declareоболочку, чтобы она только восстанавливала указанные вами переменные.
Стефан Шазелас
3

Используйте перенаправление, подстановку команд и расширение параметров. Двойные кавычки необходимы для сохранения пробелов и специальных символов. Трейлинг xсохраняет завершающие символы новой строки, которые в противном случае были бы удалены в подстановке команд.

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}
choroba
источник
Он, вероятно, хочет сохранить имя переменной также в файл.
user80551
2

Сериализировать все - POSIX

В любой оболочке POSIX вы можете сериализовать все переменные окружения с помощью export -p. Это не включает неэкспортированные переменные оболочки. Вывод правильно указан в кавычках, так что вы можете прочитать его обратно в той же оболочке и получить точно такие же значения переменных. Вывод может быть недоступен для чтения в другой оболочке, например, ksh использует $'…'синтаксис не POSIX .

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

Сериализация некоторых или всех - ksh, bash, zsh

Ksh (как 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в контексте, где вы хотите использовать значения переменных, следя за тем, чтобы переменные, которые были глобальными при экспорте, были объявлены как глобальные. Если вы хотите прочитать значения внутри функции и экспортировать их, вы можете объявить временный псевдоним или функцию. В зш:

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?
Жиль "ТАК - перестань быть злым"
@ Жиль Я подумал об этом после того, как Стефани предложила метод функции, но я не был уверен, как переносить необходимые параметры раскрытия псевдонимов между оболочками. Может быть, вы могли бы включить это в свой ответ как жизнеспособную альтернативу функции, которую я включил.
Калеб
0

Большое спасибо @ stéphane-chazelas, который указал на все проблемы с моими предыдущими попытками, теперь кажется, что это работает для сериализации массива в stdout или в переменную.

Этот метод не выполняет синтаксический анализ ввода (в отличие от declare -a/ declare -p) и поэтому безопасен от злонамеренной вставки метасимволов в сериализованный текст.

Примечание. Символы новой строки не экранируются, поскольку readудаляют \<newlines>пару символов, поэтому -d ...вместо этого они должны быть переданы для чтения, а затем сохраненные символы новой строки сохраняются.

Все это управляется в unserialiseфункции.

Используются два магических символа: разделитель полей и разделитель записей (чтобы несколько массивов можно было сериализовать в один поток).

Эти символы могут быть определены как FSи, RSно ни один из них не может быть определен как newlineсимвол, потому что экранированная новая строка удалена read.

Экранирующий символ должен быть \обратной косой чертой, так как именно он используется, readчтобы избежать распознавания IFSсимвола как символа.

serialiseбудет сериализовать "$@"в стандартный вывод, 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"'
Стефан Шазелас
черт возьми, ты прав! Я изменю свой ответ, чтобы не использовать 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
aleb
источник
-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
mikeserv
источник
Я не уверен, что вы пытались написать в первой команде. $$это PID запущенной оболочки, вы получили неправильное цитирование и что- \$то в этом роде? Можно использовать базовый подход к использованию документа здесь, но это хитрый, а не однострочный материал: что бы вы ни выбрали в качестве конечного маркера, вы должны выбрать что-то, что не появляется в строке.
Жиль "ТАК - перестань быть злым"
Вторая команда не работает, когда $VARсодержит %. Третья команда не всегда работает со значениями, содержащими несколько строк (даже после добавления явно пропущенных двойных кавычек).
Жиль "ТАК - перестань быть злым"
@ Жиль - я знаю, что это pid - я использовал его как простой источник установки уникального разделителя. Что вы подразумеваете под «не всегда» точно? И я не понимаю, чего не хватает в двойных кавычках - все это переменные присваивания. Двойные кавычки только запутывают ситуацию в этом контексте.
mikeserv
@ Жиль - я убираю задание - это аргумент env. Мне все еще любопытно, что вы имеете в виду по поводу нескольких строк - sedудаляет каждую строку до встречи VAR=до последней - так что все строки $VARпередаются дальше. Можете ли вы привести пример, который ломает его?
mikeserv
Ах, извините, третий метод работает (с исправлением цитирования). Ну, предполагая , что имя переменной (здесь VAR) не изменяется PWDили _или , возможно , другие , что некоторые оболочки определяют. Второй метод требует bash; выходной формат -vне стандартизирован (ни один из dash, 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"

Это время выше проверено.

vadimbog
источник
Я вижу, ты не проверял! Основная логика работает, но это не сложный момент. Трудная часть состоит в том, чтобы правильно процитировать вещи, а вы этого не делаете. Попробуйте переменные, значения которых содержат символы новой строки, ', *и т.д.
Жиля SO- перестать быть злом "
echo "$LVALUE=\"$RVALUE\""Предполагается также сохранить символы новой строки, и результат в файле cfg_file должен быть таким: MY_VAR1 = "Line1 \ nLine 2". Таким образом, когда eval MY_VAR1, он также будет содержать новые строки. Конечно, у вас могут возникнуть проблемы, если ваше сохраненное значение содержит сам "символ. Но об этом тоже можно позаботиться.
Вадимбог
1
Кстати, зачем голосовать «за» то, что правильно отвечает на вопрос, заданный здесь? Выше работает очень хорошо для меня и используется везде в моих сценариях?
Вадимбог