summaryrefslogtreecommitdiff
path: root/wordpress/export-json.bash
blob: fc658623d995c3ea27da83b47136b27b32ce69b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#!/bin/bash
#
# Synopsis:
#
#   #!/bin/bash
#   . export-json.bash
#   export_JSON <variables>
#
# Example:
#
#   $ (
#           source export-json.bash &&
#           export_JSON SSH_TTY \
#                       SSH_CONNECTION \
#                       SSH_CLIENT \
#                       SSH_AUTH_SOCK
#     )
#
#   {
#     "SSH_TTY": "/dev/pts/0",
#     "SSH_CONNECTION": "192.0.2.140 41564 198.51.100.25 22",
#     "SSH_CLIENT": "192.0.2.140 41564 22",
#     "SSH_AUTH_SOCK": "/tmp/ssh-XXXXcePKJl/agent.36326"
#   }
#
# This bash function exports shell variables as
# JSON ("javascript object notation") strings.
#
# The string is printed to stdout.
#
# JSON is a format that represents data as
# Javascript source code so modern programmers
# can read it unlike bash source code.
#
# Variable names are given to the function as its
# argument list:
#
#       $ source export-json.bash &&
#         export_JSON PATH
#
# The output is a single JSON object containing
# key-value mappings between JSON strings:
#
#       {
#         "PATH": "/usr/local/sbin:[...]"
#       }
#
# It is possible to use a different name for the
# variable in the JSON object key field than in
# the shell environment, like this:
#
#       $ export_JSON DBUS_SESSION_BUS_ADDRESS=p
#       {
#         "p": "unix:path=/run/user/1000/bus"
#       }
#
# It uses the external tool "jq" to parse string
# values placed in jq's argument list by bash and
# then encode them as JSON string values. This
# is no accidental dependency. The jq program is
# the foundation of the trustworthiness of this
# code. If we were encoding JSON strings in bash
# we would have to be a lot more careful.

arg1_to_env0()
{
	case "$1" in
		*=*=* )
			set -- "${1#*=}"
			echo "Error: invalid variable: ${1@Q}" >&2
			return 30
			;;
		*[^a-zA-Z0-9_=]* )
			echo "Error: invalid variable: ${1@Q}" >&2
			return 10
			;;
		[a-zA-Z_]* )
			;;
		* )
			echo "Error: invalid variable: ${1@Q}" >&2
			return 20
			;;
	esac
	set -- "${1##*=}" "${1%%=*}"
	if [ -v "$2" ]
	then
		printf '%s=%s\0' "$1" "${!2}"
	else
		echo "Warning: ignoring unset variable: ${2@Q}" >&2
	fi
}

# Hygienic loop.  Doesn't touch the shell
# environment.
for_each()
{
	while [ $# -ge 2 ]
	do
		$1 "$2" || return
		set -- "$1" "${@:3}"
	done
}

to_JSON_all()
{
	jq -s 'add' < \
		<(
			if [ "$1" = '-a' ]
			then
				jq_env
			fi
			for_each to_JSON1 $(compgen -A arrayvar)
		)
}

jq_zip2()
{
	cat <<'END'
$ARGS.positional |
[
  .[ length/2 : ],
  .[ : length/2 ]
] |
transpose |
map ({ (.[0]): .[1] }) |
add
END
}

json_encode_associative_array()
{
	eval \
		'set -- "$1" \
			"${'"$1"'[@]}" \
			"${!'"$1"'[@]}";
		' &&
	if [ $# -ge 3 ]
	then
		jq -n "{ (\$k): $(jq_zip2) }" \
			--arg k "$1" \
			--args "${@:2}"
	else
		jq -n '{ ($k): {} }' --arg k "$1"
	fi
}

json_encode_indexed_array()
{
	eval \
		'set -- "$1" \
			"${'"$1"'[@]}" \
		' &&
	jq -n '{ ($k): $ARGS.positional }' \
		--arg k "$1" \
		--args "${@:2}"
}

# This uses the more complex implementation that,
# when used with multiple arguments, does not
# require extra calls to jq. Since this is only
# using a single argument, a more straightforward
# implementation could be used just as well.
json_encode_string()
{
	[ -v "$1" ] &&
	export_JSON_unsafe "$1"
}

# The straightforward implementation:
json_encode_string()
{
	[ -v "$1" ] &&
	jq -n '{ ($k): $v }' --arg k "$1" --arg v "${!1}"
}
# But is having two implementations really
# straightforward?  And what about this
# parenthetical commentary? Moreso wobbly, if not
# winding, error-prone complexity finding itself
# grinding up on the accidents of time passing it
# might be anyway but the takeaway is don't touch
# it till it croak, the tests will run and the
# result will speak, may truth spread through the
# dendrites of Samizdat!

to_JSON()
{
	case "$#$1" in
		0 )
			printf \
				'%s\n' \
				'usage: to_JSON -a' \
				'       to_JSON -A' \
				'       to_JSON <variable name> [...]' \
				>&2
			return -1
			;;
		1-a | 1-A )
			to_JSON_all $1
			;;
		* )
			for_each to_JSON1 "$@"
			;;
	esac
}

to_JSON1()
{
	case "${!1@a}" in
		*a* )
			json_encode_indexed_array "$1"
			;;
		*A* )
			json_encode_associative_array "$1"
			;;
		* )
			json_encode_string "$1"
			;;
	esac
}

env0_to_JSON()
{
	set --
	while read -d ''
	do
		set -- "$@" --arg "${REPLY%%=*}" "${REPLY#*=}"
	done
	jq -n -r '$ARGS.named' "$@"
}

export_JSON_unsafe()
{
	(
		set -o pipefail
		for_each arg1_to_env0 "$@" | env0_to_JSON
	)
}

safety_pipe()
{
	[ $# -ge 2 ] || return
	set -- "$(mktemp)" "$@" || return
	(
		trap "rm ${1@Q}" EXIT
		"${@:3}" > "$1" &&
		$2 < "$1"
	)
}

export_JSON()
{
	safety_pipe env0_to_JSON for_each arg1_to_env0 "$@"
}

filter_vars()
{
	while read
	do
		if [ -v "$REPLY" ]
		then
			printf '%s\n' "$REPLY"
		fi
	done
}

jq_env()
{
	export_JSON $(compgen -v | filter_vars) | jq "$@"
}

jq_exports()
{
	export_JSON $(compgen -e | filter_vars) | jq "$@"
}

try()
{
	"$@"
	printf '(Exit %s) <- [%s]\n' "$?" "${*@Q}" >&2
}

runtest()
{
	set -- SSH_CLIENT SSH_TTY SSH_AUTH_SOCK SSH_CONNECTION
	try export_JSON "$@"
	unset unsetvar
	try export_JSON SSH_TTY unsetvar
	try export_JSON unsetvar SSH_TTY
	try export_JSON
	try export_JSON ''
	try export_JSON 'invalid!' SSH_TTY
	try export_JSON SSH_TTY 'invalid!'
	try jq_env .unsetvar
	emptyvar= try jq_env .emptyvar
	try jq_exports '.|{TERM,LANG,HOSTTYPE,EDITOR,SHELL}'
	try jq_env '.|{TERM,LANG,HOSTTYPE,EDITOR,SHELL}'
	try to_JSON PATH BASH_ARGV BASH_VERSINFO BASH_ALIASES BASH_CMDS
}

# code poetry flowetry toiletry coiled spring load
# the home row write tight c with terry davis down
# in a basement univalent foundation concrete on
# the bottom with donald while they all on top of
# things they found up there still we founding
# here building tall stacks of calls out to all to
# any accepting returns of types unknown or known
#
# defensively typed values for anomic millennial
# hackers
#
# atomized castaways of aging social bodies
# reviving ancient code bodies encoding ancient
# knowns into own bodies endomorphing endorphins
# of discovery over arrows of time chance and
# finger dance into machine flow that won't halt
# or falter to a byzantine emperors structures
# span relativities messages