bashまたはzshでシェル変数をシリアル化する


12

シェル変数をシリアル化する方法はありますか?変数があり$VAR、それをファイルなどに保存して、後で同じ値を取得するために読み直したいとしますか?

これを行うポータブルな方法はありますか?(私はそうは思わない)

bashまたはzshでそれを行う方法はありますか?


2
注意:先日受け入れた私の回答のバージョンにはいくつかのシナリオで壊れる重大な問題がありました。修正を含めて(および機能を追加して)書き直しました。実際に最初から読み直し、修正バージョンを使用するようにコードを移植する必要があります。
カレブ14年

^ Calebの市民権のもう1つの例。
mikeserv 14年

回答:


14

警告:これらのソリューションでは、スクリプトでシェルコードとして実行されるため、データファイルの整合性が安全であることを信頼していることに注意する必要があります。それらを保護することは、スクリプトのセキュリティにとって最も重要です!

1つ以上の変数をシリアル化するためのシンプルなインライン実装

はい。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にはtypesetkshとの互換性のためにここで使用するエイリアスがあります。

関数を使用したより複雑な一般化された実装

上記の実装は非常に単純ですが、頻繁に呼び出す場合は、ユーティリティ関数を使用して簡単にすることができます。さらに、カスタム関数内に上記を含めようとすると、変数スコープの問題が発生します。このバージョンでは、これらの問題が解消されるはずです。

我々は両方の場合固定されバッシュ/ zshの相互互換性を維持するために、これらの全てのためのメモ、typesetおよびdeclareコードので、どちらかまたは両方のシェルで動作すべきです。これにより、1つのシェルまたは別のシェルに対してのみこれを行っていた場合に除去できるいくつかのバルクと混乱が追加されます。

このために関数を使用する(または他の関数にコードを含める)場合の主な問題は、typeset関数がコードを生成し、関数内からスクリプトにソースを戻すと、デフォルトではグローバル変数ではなくローカル変数を作成することです。

これは、いくつかのハックの1つで修正できます。これを修正する最初の試みは、シリアル化プロセスの出力を解析しsed-gフラグを追加し、作成されたコードがソースに戻されたときにグローバル変数を定義することでした。

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

ファンキーなsed式は、「typeset」または「declare」の最初の出現にのみ一致-gし、最初の引数として追加することに注意してください。StéphaneChazelasがコメントで正しく指摘したように、そうでない場合は、直列化された文字列にリテラルの改行とその後に単語declareまたはtypesetが含まれる場合にも一致するため、最初の出現のみを一致させる必要があります。

私の最初の解析の偽のパスを修正することに加えて、Stéphane は、文字列の解析の問題を回避するだけでなく、ラッパー関数を使用してアクションを再定義することで追加機能を追加する便利なフックになる可能性がある、これをハッキングするより脆弱な方法も提案しましたこれは、declareコマンドまたはtypesetコマンドで他のゲームをプレイしていないことを前提としていますが、この機能を独自の別の機能の一部として含める場合は、この手法を実装する方が簡単です。書き込まれているデータと、-gフラグが追加されているかどうかを制御できませんでした。エイリアスを使用して同様のことを行うこともできます。実装に関するGillesの回答を参照してください。

結果をさらに便利にするために、引数配列内の各単語が変数名であると想定して、関数に渡される複数の変数を反復処理できます。結果は次のようになります。

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のはtypesetbash、その点でzshもサポートしているtypesetため、typesetより移植性があります。export -pはPOSIXですが、引数を取らず、その出力はシェルに依存します(ただし、POSIXシェルに対して適切に指定されているため、たとえばbashまたはkshがとして呼び出される場合sh)。変数を引用することを忘れないでください。ここでsplit + glob演算子を使用しても意味がありません。
ステファンシャゼル14年

-EいくつかのBSDでのみ見られることに注意してくださいsed。変数値には改行文字が含まれる場合があるため、sed 's/^.../.../'が正常に機能することは保証されません。
ステファンシャゼル14年

これはまさに私が探していたものです!シェル間で変数をやり取りする便利な方法が必要でした。
fwenom 14年

私は意味:a=$'foo\ndeclare bar' bash -c 'declare -p a'意志の出力に始まる行をインストールするためにdeclaredeclare() { builtin declare -g "$@"; }呼び出す前にsource(そしてその後、設定を解除して)行うのがおそらく良いでしょう
ステファンシャゼル14年

2
@Gilles、エイリアスは関数内では機能せず(関数定義時に定義する必要があります)、bashを使用すると、shopt -s expandalias対話的でないときにaを行う必要があります。関数を使用すると、declareラッパーを拡張して、指定した変数のみを復元することもできます。
ステファンシャゼラス14年

3

リダイレクト、コマンド置換、およびパラメーター拡張を使用します。空白と特殊文字を保持するには、二重引用符が必要です。末尾xは、コマンドの置換で削除されない末尾の改行を保存します。

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

彼はおそらくファイルにも変数名を保存したいと思うでしょう。
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変数の値を使用するコンテキストで実行する必要があります。エクスポート時にグローバルであった変数がグローバルとして再宣言されるように注意してください。関数内の値を読み戻し、エクスポートする場合は、一時的なエイリアスまたは関数を宣言できます。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
}

読み取り専用変数を許可するシェルでは、読み取り専用の変数を読み戻そうとするとエラーが発生することに注意してください。


これは、$PWDand などの変数をもたらし$_ます-以下の独自のコメントを参照してください。
mikeserv 14年

@Caleb typesetのエイリアスはtypeset -gどうですか?
ジル 'SO-悪であるのをやめる' 14年

@Gillesステファニーが関数メソッドを提案した後、そのことを考えましたが、シェル間で必要なエイリアス展開オプションを移植可能に設定する方法がわかりませんでした。たぶん、私が含めた機能の実行可能な代替として、あなたの答えにそれを入れることができます。
カレブ14年

0

ずっと私の以前の試みですべての問題を指摘@ステファン・chazelasのおかげで、これは今stdoutにまたは変数に配列をシリアライズするために動作するようです。

この手法は、(declare -a/ とは異なりdeclare -p)入力をシェル解析しないため、シリアル化されたテキストへのメタキャラクターの悪意のある挿入に対して安全です。

注:文字ペアをread削除するため、改行はエスケープされません。その\<newlines>ため、-d ...代わりにreadに渡す必要があり、エスケープされていない改行は保持されます。

これらはすべてunserialise機能で管理されます。

2つのマジックキャラクター、フィールドセパレーターとレコードセパレーターが使用されます(複数の配列を同じストリームにシリアル化できるようにするため)。

これらの文字は、次のように定義することができますFSし、RSどちらもとして定義することができnewlineエスケープ改行がによって削除されているので文字read

エスケープ文字\はバックスラッシュである必要があります。これはread、文字が文字として認識されるのを避けるために使用されるものですIFS

serialiseシリアライズされます"$@"stdoutに、serialise_toで指定さvarableにシリアライズされます$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は(-rフラグを渡さない限り)readエスケープ文字を尊重し\て、入力フィールドの区切りや行区切りなどの文字の特別な意味を削除します。

単なる引数リストの代わりに配列をシリアル化する場合は、配列を引数リストとして渡すだけです。

serialise_array "${my_array[@]}"

unserialiseループで使用できますがread、これは単にラップされた読み取りであるためです。ただし、ストリームは改行で区切られていないことに注意してください。

while unserialise array
do ...
done

要素は、その後のように(現在のロケールにおける)印刷不能またはコントロールTABのような文字や改行が含まれている場合は動作しませんbashzsh、それらをレンダリングします$'\xxx'。してみbash -c $'printf "%q\n" "\t"'たりbash -c $'printf "%q\n" "\u0378"'
ステファンChazelas

くそー、あなたは正しい!代わりに空白をエスケープするためにprintf%qではなく$ {@ // .. / ..}反復を使用するように回答を変更します
サムリドコット

このソリューションは、$IFS変更されていないことに依存しており、空の配列要素を適切に復元できなくなりました。実際、IFSの異なる値を使用し、-d ''改行をエスケープする必要を回避するために使用する方が理にかなっています。たとえば:、フィールドセパレータとして使用IFS=: read -ad '' arrayし、それとバックスラッシュのみをエスケープしてインポートに使用します。
ステファンシャゼル

うん....読み取りでフィールド区切り文字として使用する場合、特別な処理を空白に折り畳むことを忘れていました。今日はあなたがボールに乗ってくれてうれしいです!\ nのエスケープを避けるために-d ""については正しいですが、私の場合は、シリアル化のストリームを読みたかったのですが、答えを調整します。ありがとう!
サムリッドコット

改行をエスケープすると、それを保存することができなくなり、一度消えreadます。backslash-newline for 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 

最初のオプションと2番目のオプションは、変数の値に文字列が含まれていないと仮定して、POSIXシェルで機能します。

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

3番目のオプションは、POSIXシェルで機能するはずですが、_またはなどの他の変数を定義しようとする場合がありますPWD。ただし、定義しようとする可能性のある変数はシェル自体によって設定および維持されるだけです。たとえばexport、それらのいずれかの値をimport しても$PWD、シェルは単にそれらをリセットします。とにかく正しい値をすぐに-やってみてPWD=any_value、自分で確かめてください。

そして-少なくともGNUの場合bash-デバッグ出力はシェルへの再入力のために自動的に安全に引用されるため、これ'"$VAR":内のハード引用の数に関係なく機能します

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

$VAR 後で次のパスが有効なスクリプトで保存された値に設定できます。

. ./VAR.file

最初のコマンドで何を書き込もうとしたかわかりません。$$実行中のシェルのPIDは、引用を間違えたり、意地悪し\$たりしましたか?ヒアドキュメントを使用する基本的なアプローチは機能する可能性がありますが、1ライナーの素材ではなく、トリッキーです。エンドマーカーとして選択したものは、文字列に表示されないものを選択する必要があります。
ジル 'SO-悪であるのをやめる'

次を$VAR含む場合、2番目のコマンドは機能しません%。3番目のコマンドは、複数行を含む値に対して常に動作するとは限りません(明らかに欠落している二重引用符を追加した後でも)。
ジル 'SO-悪であるのをやめる'

@Gilles-pidを知っています-一意の区切り文字を設定する単純なソースとして使用しました。「常にそうではない」とはどういう意味ですか?そして、二重引用符が欠落していることを理解していません-それらはすべて変数の割り当てです。二重引用符は、そのコンテキストの状況を混乱させるだけです。
mikeserv

@Gilles-私は割り当て物を撤回します-それはに対する議論envです。私はまだ複数行についてあなたが何を意味するのか興味があります- 最後sedまで遭遇するVAR=まですべての行を削除します-それですべての行が$VAR渡されます それを破る例を提供してもらえますか?
mikeserv

ああ、おologiesび申し上げます、3番目の方法は機能します(クォートの修正あり)。さて、変数名(ここではVAR)が変更されていないPWDか、_またはいくつかのシェルで定義されている他の変数が変更されていないと仮定します。2番目の方法ではbashが必要です。からの出力形式-vは標準化されていません(ダッシュ、ksh93、mksh、zshのいずれも機能しません)。
ジル 'SO-悪であるのをやめる' 14年

-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"したがって、MY_VAR1を評価すると、改行も含まれます。もちろん、格納された値自体に"char が含まれている場合、問題が発生する可能性があります。しかし、それも同様に注意することができます。
vadimbog 14年

1
ところで、なぜここで尋ねられた質問に正しく答えている何かを投票するのですか?上記は私にとって非常にうまく機能し、スクリプトのどこでも使用できますか?
vadimbog 14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.