Skip to content

Instantly share code, notes, and snippets.

@Lohann
Last active July 27, 2026 09:13
Show Gist options
  • Select an option

  • Save Lohann/9ea5c2b4c2c224e17950d87ed1ab9459 to your computer and use it in GitHub Desktop.

Select an option

Save Lohann/9ea5c2b4c2c224e17950d87ed1ab9459 to your computer and use it in GitHub Desktop.
#!/bin/sh
# locals_snapshot
# ----------------------
# An over-engineered Sed script that parses the output of 'set' builtin and format
# one variable per line (even when it's value contains multiple lines), to handle
# newlines and raw bytes, all values are formated in C printf format:
# https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap05.html#tag_05
#
# # Why?
# The command `set` display all local variables in the shell, but unfortunatelly its
# output isn't standartized between different shells, so there's no portable solution
# to extract it's output, even check the variable existence is not trivial once multi
# line values may cause false positives, ex: v='foo=value<NEW LINE>bar=value'. The sed
# script below is a minimal shell script parser, it unquotes strings, parses escaped
# characters, extracting the variable's original value, then convert multi-line values
# to single line printf-like format.
#
# # USAGE
# Example 01: Compare variables before and after executing some code.
# ```
# before=`take_snapshot`
# <alter some variables>
# after=`take_snapshot`
# printf '%s\n\n' "$before" "$after"
# ```
#
# Example 02: Filter by variable name.
# ```
# snapshot=`take_snapshot | grep '^VARNAME'`
# ```
#
# Example 03: List varname and value (obs: loses original value trailing spaces)
# ```
# snapshot=`take_snapshot`
# varname= value= ifs_backup=$IFS IFS='
# '
# for line in $snapshot; do
# varname=`expr "X$line" : 'X\([^=]*\)=.*'`
# if value=`expr "X$line" : '[^=]*=\(.*\)'`
# then eval "value=\`printf $value\`"
# else value=
# fi
# printf '%s\n' "varname=$varname, value=$value"
# done
# IFS=$ifs_backup
# ```
#
# Example 04: Parse original value (keeps trailing spaces)
# ```
# snapshot=`take_snapshot`
# varname= quoted= value= line= ifs_backup=$IFS IFS='
# '
# for line in $snapshot
# do
# IFS=$ifs_backup varname= quoted= value=
# varname=`expr "X$line" : 'X\([^=]*\)=.*'` || n=
# if quoted=`expr "X$line" : '[^=]*=\(.*\)'`; then
# # 1 - Use printf to extract the original value
# # 2 - Use sed to quote the original value (otherwise trailing spaces are automatically removed)
# # 3 - Use eval to assign the quoted value to a variable.
# value="printf x${quoted}x | LC_ALL=C sed -e \"s/'/'\\\\\\\\''/g\" -e \"1s/^x/'/\" -e \"\\\$s/x\\\$/'/\""
# value=`eval "$value"` && eval "value=$value" || value=
# else quoted= value=
# fi
# printf '%s\n' "varname=$varname, quoted=$quoted, value=$value"
# done
# IFS=$ifs_backup
# ```
#
# Note: Doesn't parse bash's 'Indexed Array' and 'Associative Array'.
# @author Lohann Ferreira <developer@lohann.dev>
take_snapshot()
{
# save Sed Script as positional argument (avoiding overwrite global variables)
#
# Blame Lee E. McMahon (1931-1989) for sed's syntax. :-)
set x '{
/^[[:space:]]*#/b
s/^[^=]*$/&=/
/^[_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]*=/!b
/^[^=]*=(/{
:skip
/)[[:space:]]*$/!{
$!b fail
N
b skip
}
d
b
}
:clear1
t clear1
s/^\([^=]*=\)[[:space:]]*$/\1/
t end
/^[^=]*=/{
h
s/^\([^=]*=\).*$/\1/
x
s/^[^=]*=//
b parse
}
b fail
:parse
t parse
/^$/b end
/^[[:space:]]/b fail
s/^'\''//
t quote
s/^"//
t dquote
s/^\$'\''//
t ansi
s/^\\'\''/'\''\nq'\''/
t plain
s/^\\\(.\)/'\''\1'\''/
t plain
s|^[/0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:=.,%@?!+-]\{1,\}|'\''&'\''|
t plain
b fail
:plain
H
s/^'\''[^'\'']*'\''//
x
s/^\([^'\'']*\)[^'\'']'\''\([^'\'']*\)'\''.*$/\1\2/
x
b parse
:ansi
t ansi
s/^'\''//
t parse
s/^[^'\''\\]\{1,\}/'\''&'\''/
t next
s/^\\//
t clear
b fail
:clear
s/^'\''/'\''\nq'\''/
t next
s/^[\\abeEtnvfr]/'\''\n&'\''/
t next
s/^x[0123456789ABCDEFabcdef][0123456789ABCDEFabcdef]/'\''\n&'\''/
s/^x\([0123456789ABCDEFabcdef]\)/'\''\nx0\1'\''/
t next
s/^U[0123456789ABCDEFabcdef]\{1,8\}/'\''\n&U'\''/
t next
s/^u[0123456789ABCDEFabcdef]\{1,4\}/'\''\n&u'\''/
t next
s/^0[01234567][01234567][01234567]/'\''\no&o'\''/
s/^[01234567][01234567][01234567]/'\''\no&o'\''/
s/^[01234567][01234567]/'\''\no0&o'\''/
s/^[01234567]/'\''\no00&o'\''/
t next
s/^C-//
t ctrl
s/^C//
t ctrl
s/^M-//
t mctrl
s/^M//
t mctrl
b fail
:mctrl
t mctrl
s/^@/'\''\no300o'\''/
s/^A/'\''\no301o'\''/
s/^B/'\''\no302o'\''/
s/^C/'\''\no303o'\''/
s/^D/'\''\no304o'\''/
s/^E/'\''\no305o'\''/
s/^F/'\''\no306o'\''/
s/^G/'\''\no307o'\''/
s/^H/'\''\no310o'\''/
s/^I/'\''\no311o'\''/
s/^J/'\''\no312o'\''/
s/^K/'\''\no313o'\''/
s/^L/'\''\no314o'\''/
s/^M/'\''\no315o'\''/
s/^N/'\''\no316o'\''/
s/^O/'\''\no317o'\''/
s/^P/'\''\no320o'\''/
s/^Q/'\''\no321o'\''/
s/^R/'\''\no322o'\''/
s/^S/'\''\no323o'\''/
s/^T/'\''\no324o'\''/
s/^U/'\''\no325o'\''/
s/^V/'\''\no326o'\''/
s/^W/'\''\no327o'\''/
s/^X/'\''\no330o'\''/
s/^Y/'\''\no331o'\''/
s/^Z/'\''\no332o'\''/
s/^\[/'\''\n3033o'\''/
s/^\\/'\''\n3034o'\''/
s/^\]/\no335o'\''/
s/^\^/'\''\no336o'\''/
s/^_/'\''\no337o'\''/
s/^[?]/'\''\no377o'\''/
t next
b fail
:ctrl
t ctrl
s/^@/'\''\no000o'\''/
s/^A/'\''\no001o'\''/
s/^B/'\''\no002o'\''/
s/^C/'\''\no003o'\''/
s/^D/'\''\no004o'\''/
s/^E/'\''\no005o'\''/
s/^F/'\''\no006o'\''/
s/^G/'\''\na'\''/
s/^H/'\''\nb'\''/
s/^I/'\''\nt'\''/
s/^J/'\''\nn'\''/
s/^K/'\''\nv'\''/
s/^L/'\''\nf'\''/
s/^M/'\''\nr'\''/
s/^N/'\''\no016o'\''/
s/^O/'\''\no017o'\''/
s/^P/'\''\no020o'\''/
s/^Q/'\''\no021o'\''/
s/^R/'\''\no022o'\''/
s/^S/'\''\no023o'\''/
s/^T/'\''\no024o'\''/
s/^U/'\''\no025o'\''/
s/^V/'\''\no026o'\''/
s/^W/'\''\no027o'\''/
s/^X/'\''\no030o'\''/
s/^Y/'\''\no031o'\''/
s/^Z/'\''\no032o'\''/
s/^\[/'\''\ne'\''/
s/^[\\]/'\''\no034o'\''/
s/^\]'\''/\no035o'\''/
s/^\^/'\''\no036o'\''/
s/^_/'\''\no037o'\''/
s/^[?]/'\''\no177o'\''/
t next
b fail
:next
t next
/^'\''[^'\'']*'\''/!{
b fail
}
H
s/^'\''[^'\'']*'\''//
x
s/^\([^'\'']*\)[^'\'']'\''\([^'\'']*\)'\''.*$/\1\2/
x
b ansi
:quote
t quote
/^$/{
$b fail
x
s/$/\nn/
x
n
b quote
}
s/^'\''//
t parse
s/^[^'\'']*'\''/'\''&/
t plain
/^[^'\'']*[^'\'']/{
s/^\([^'\'']*[^'\'']\)/'\''&'\''/
H
s/^'\''[^'\'']*'\''//
x
s/^\([^'\'']*\)[^'\'']'\''\([^'\'']*\)'\''.*$/\1\2/
x
b quote
}
b fail
:dquote
t dquote
s/^"//
t parse
s/^[^'\''"\\]\{1,\}/'\''&'\''/
t dnext
s/^'\''/'\''\nq'\''/
t dnext
s/^\\\([\\$"`]\)/'\''\1'\''/
t dnext
/^\\$/{
s/^.*$//
$b fail
n
b dquote
}
s/^\\/'\''&'\''/
t dnext
/^$/{
$b fail
x
s/$/\nn/
x
n
b dquote
}
b fail
:dnext
/^'\''[^'\'']*[^'\'']'\''/!{
b fail
}
H
s/'\''[^'\'']*'\''//
x
s/^\([^'\'']*\)[^'\'']'\''\([^'\'']*\)'\''.*$/\1\2/
x
b dquote
:fail
s/\n/&n/g
s/'\''/\nq/g
s/^/'\''/
H
s/^.*$//
x
s/^\([^'\'']*\)[^'\'']'\''\(.*\)$/\1\2/
s/\nq/\\'\''/g
s/\n\([abtnvfr]\)/\\\1/g
s/\no\([^o]*\)o/\\\1/g
s/\n\(x..\)/\\\1/g
s/\n\(u[^u]*\)u/\\\1/g
s/\n\(U[^U]*\)U/\\\1/g
s/^.*$/# invalid -> $'\''&'\''/
p
q
:end
s/\n/&n/g
s/'\''/\nq/g
s/^/'\''/
H
s/^.*$//
x
s/^\([^'\'']*\)[^'\'']'\''\(.*\)$/\1\2/
s/\\/\\&/g
s/%/&%/g
s/\n/'\''/g
s/\x01/'\''o001o/g
s/\x02/'\''o002o/g
s/\x03/'\''o003o/g
s/\x04/'\''o004o/g
s/\x05/'\''o005o/g
s/\x06/'\''o006o/g
s/\x07/'\''o007o/g
s/\x08/'\''o010o/g
s/\x09/'\''t/g
s/\x0b/'\''o013o/g
s/\x0c/'\''o014o/g
s/\x0d/'\''o015o/g
s/\x0e/'\''o016o/g
s/\x0f/'\''o017o/g
s/\x10/'\''o020o/g
s/\x11/'\''o021o/g
s/\x12/'\''o022o/g
s/\x13/'\''o023o/g
s/\x14/'\''o024o/g
s/\x15/'\''o025o/g
s/\x16/'\''o026o/g
s/\x17/'\''o027o/g
s/\x18/'\''o030o/g
s/\x19/'\''o031o/g
s/\x1a/'\''o032o/g
s/\x1b/'\''o033o/g
s/\x1c/'\''o034o/g
s/\x1d/'\''o035o/g
s/\x1e/'\''o036o/g
s/\x1f/'\''o037o/g
s/'\''a/\\007/g
s/'\''b/\\010/g
s/'\''t/\\t/g
s/'\''n/\\n/g
s/'\''v/\\013/g
s/'\''f/\\014/g
s/'\''r/\\015/g
s/'\''[eE]/\\033/g
s/'\''\(x..\)/\\\1/g
s/'\''o\([^o]*\)o/\\\1/g
s/'\''\(u[^u]*\)u/\\\1/g
s/'\''\(U[^U]*\)U/\\\1/g
s/'\''q/'\''\\'\'''\''/g
s/^\([^=]*=\)\(.*\)/\1'\''\2'\''/
s/'\'''\''$//
p
}' || { echo "take_snapshot: builtin 'set' failed with status $?" >&2; return 125; }
test "$#" -eq 2 || { echo "take_snapshot: unexpected arg count: $# != 2" >&2; return 125; }
if test "${ZSH_VERSION+y}" && (emulate sh) >/dev/null 2>&1
then emulate sh -c 'set | LANGUAGE=C LC_ALL=C sed -n "$2"'
else set | LANGUAGE=C LC_ALL=C sed -n "$2"
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment