シェル変数をシリアル化する方法はありますか?変数があり$VAR
、それをファイルなどに保存して、後で同じ値を取得するために読み直したいとしますか?
これを行うポータブルな方法はありますか?(私はそうは思わない)
bashまたはzshでそれを行う方法はありますか?
シェル変数をシリアル化する方法はありますか?変数があり$VAR
、それをファイルなどに保存して、後で同じ値を取得するために読み直したいとしますか?
これを行うポータブルな方法はありますか?(私はそうは思わない)
bashまたはzshでそれを行う方法はありますか?
回答:
警告:これらのソリューションでは、スクリプトでシェルコードとして実行されるため、データファイルの整合性が安全であることを信頼していることに注意する必要があります。それらを保護することは、スクリプトのセキュリティにとって最も重要です!
はい。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との互換性のためにここで使用するエイリアスがあります。
上記の実装は非常に単純ですが、頻繁に呼び出す場合は、ユーティリティ関数を使用して簡単にすることができます。さらに、カスタム関数内に上記を含めようとすると、変数スコープの問題が発生します。このバージョンでは、これらの問題が解消されるはずです。
我々は両方の場合固定されバッシュ/ 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
のはtypeset
。bash
、その点でzsh
もサポートしているtypeset
ため、typeset
より移植性があります。export -p
はPOSIXですが、引数を取らず、その出力はシェルに依存します(ただし、POSIXシェルに対して適切に指定されているため、たとえばbashまたはkshがとして呼び出される場合sh
)。変数を引用することを忘れないでください。ここでsplit + glob演算子を使用しても意味がありません。
-E
いくつかのBSDでのみ見られることに注意してくださいsed
。変数値には改行文字が含まれる場合があるため、sed 's/^.../.../'
が正常に機能することは保証されません。
a=$'foo\ndeclare bar' bash -c 'declare -p a'
意志の出力に始まる行をインストールするためにdeclare
。declare() { builtin declare -g "$@"; }
呼び出す前にsource
(そしてその後、設定を解除して)行うのがおそらく良いでしょう
shopt -s expandalias
対話的でないときにaを行う必要があります。関数を使用すると、declare
ラッパーを拡張して、指定した変数のみを復元することもできます。
POSIXシェルでは、すべての環境変数をでシリアル化できますexport -p
。これには、エクスポートされていないシェル変数は含まれません。出力は適切に引用符で囲まれているため、同じシェルで読み戻して、まったく同じ変数値を取得できます。出力は、別のシェルでは読み取れない場合があり$'…'
ます。たとえば、kshは非POSIX 構文を使用します。
save_environment () {
export -p >my_environment
}
restore_environment () {
. ./my_environment
}
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 () { … }
ます。
さらに制御したい場合は、変数のコンテンツを手動でエクスポートできます。変数の内容を正確にファイルに出力するには、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
and などの変数をもたらし$_
ます-以下の独自のコメントを参照してください。
typeset
のエイリアスはtypeset -g
どうですか?
ずっと私の以前の試みですべての問題を指摘@ステファン・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
bash
とzsh
、それらをレンダリングします$'\xxx'
。してみbash -c $'printf "%q\n" "\t"'
たりbash -c $'printf "%q\n" "\u0378"'
$IFS
変更されていないことに依存しており、空の配列要素を適切に復元できなくなりました。実際、IFSの異なる値を使用し、-d ''
改行をエスケープする必要を回避するために使用する方が理にかなっています。たとえば:
、フィールドセパレータとして使用IFS=: read -ad '' array
し、それとバックスラッシュのみをエスケープしてインポートに使用します。
read
ます。backslash-newline for read
は、論理行を別の物理行に継続する方法です。編集:ああ、あなたはすでに改行の問題に言及しているようです。
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ライナーの素材ではなく、トリッキーです。エンドマーカーとして選択したものは、文字列に表示されないものを選択する必要があります。
$VAR
含む場合、2番目のコマンドは機能しません%
。3番目のコマンドは、複数行を含む値に対して常に動作するとは限りません(明らかに欠落している二重引用符を追加した後でも)。
env
です。私はまだ複数行についてあなたが何を意味するのか興味があります- 最後sed
まで遭遇するVAR=
まですべての行を削除します-それですべての行が$VAR
渡されます それを破る例を提供してもらえますか?
VAR
)が変更されていないPWD
か、_
またはいくつかのシェルで定義されている他の変数が変更されていないと仮定します。2番目の方法ではbashが必要です。からの出力形式-v
は標準化されていません(ダッシュ、ksh93、mksh、zshのいずれも機能しません)。
ほとんど同じですが少し異なります:
スクリプトから:
#!/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"
この時間はテスト済みです。
'
、*
など
echo "$LVALUE=\"$RVALUE\""
改行も保持することになっているため、cfg_fileの結果は次のようになります。MY_VAR1 = "Line1 \ nLine 2"したがって、MY_VAR1を評価すると、改行も含まれます。もちろん、格納された値自体に"
char が含まれている場合、問題が発生する可能性があります。しかし、それも同様に注意することができます。