To add to @CharlesDuffy answer, and for thoses who are still stuck with old 'unupgradable' hardware/vm, here is a POSIX / old bash way to achieve that in a safe manner. Tested with dash, ksh93, bash 2.05b, 3 and 4. Can't retrieve my old Bourne shell 92.
EDIT: Thanks to useful @CharlesDuffy comments :
Updated to deal with blank/space/newline/wathever in the 'value' part. in a basic manner (multiple blanks reduced to one space, new lines swallowed). Work in progress for a better way to handle that.
Produced variable names are now prefixed with _
to prevent any
attempt to override PATH
, LD_PRELOAD
, etc.
EDIT2: Added a Bash 2/3/4 and ksh version that deals with tab/spaces/newline in values. See below.
EDIT3: Add a POSIX compliant rev 2 that can handle TAB, NEWLINE and multiple SPACE.
POSIX compliant V1 :
This one can't handle nicely newlines and tabs in the value part of the variable. It won't crash , but the related variables will be "compacted" in one line with spaces instead of newlines, and all tabs / multiple spaces are reduced to one space.
#!/bin/sh
# If you only have a bash 4.x, you can test with compat 3.1 bash
# shopt -s compat31
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1'
SYSCFG_lan_netmask='255.255.255.0' SYSCFG_space='my space' SYSCFG_newline='I have
many multi
lines input"
# An "env variable" definer that use the read command
# to parse and define the env variable
define() {
IFS=\= read -r key value <<EOF
$1
EOF
# Unquotting the value, adapt as it fit your needs
value="${value#\'}"
value="${value%\'}"
read -r "_${key}" << EOF
${value}
EOF
}
unset _SYSCFG_lan_ifname
unset _SYSCFG_lan_ipaddr
unset _SYSCFG_lan_netmask
unset _SYSCFG_space
unset _SYSCFG_newline
# Using the set command to "parse" the variables string
set ${FOO}
while [ "$1" ] ; do
key_value="$1"
while [ "$1" ] && [ "${key_value%\'}" = "${key_value}" ] ; do
shift
key_value="${key_value} $1"
done
define "${key_value}"
[ "$1" ] && shift
done
echo "${_SYSCFG_lan_ifname}"
echo "${_SYSCFG_lan_ipaddr}"
echo "${_SYSCFG_lan_netmask}"
echo "${_SYSCFG_space}"
echo "${_SYSCFG_newline}"
The output is the same with ksh93, bash 2..4, dash :
br1
10.0.0.1
255.255.255.0
my space
I have many multi lines input
POSIX compliant V2 :
This version can handle special char and, partially, newlines. It does not use the set
command to parse the string, avoiding any potential glob effect. We rely on the basic shell trimer #
and %
. This one can also handle different quoting in the string and escaped quotes/double quotes. The define
function handles multi lines through \n
in the here-doc, so their translation will be left to the script user.
#!/bin/sh
# If you only have a bash 4.x, you can test with compat 3.1 bash
# shopt -s compat31
# Test string. There is a TAB between "input" and "and".
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1 *'
SYSCFG_lan_netmask=\"255.255.255.0\" SYSCFG_space='mypath\\ so\'urce\\my space'
SYSCFG_newline='I have
many multi
lines input and /path/to/thi ngs'"
#
# Define here the prefix you want for the variables. A prefix is required
# to avoid LD_PRELOAD, PATH, etc. override from a variable.
_prefix="_"
#
# The POSIX way for a new line constant.
_NL="
"
# An "env variable" definer that use the read command to parse and define
define() {
_key="$1"
_value="$2"
_quote="$3"
_tmp=""
# The POSIX read command can only read one line at a time.
# For multiline, we loop to rebuild the full value.
while read -r _line ; do
[ "${_tmp}" ] && _tmp="${_tmp}\n${_line}" || _tmp="${_line}";
done <<EOF
${_value}
EOF
read -r "${_prefix}${_key}" << EOF
${_tmp}
EOF
}
unset _SYSCFG_lan_ifname
unset _SYSCFG_lan_ipaddr
unset _SYSCFG_lan_netmask
unset _SYSCFG_space
unset _SYSCFG_newline
# First, we trim blanks
_FOO="${FOO# * }"
_FOO="${_FOO% * }"
# We use shell basic trimer to "parse" the string
while [ "${_FOO}" ] ; do
# Get the first assignation from the beginning
_FOO_next="${_FOO#*=}"
if [ "${_FOO_next}" != "${_FOO}" ] ; then
# If there is backslash in the string we need to double escape them for
# using it as a pattern. We do that in a safe manner regarding FOO content.
_FOO_next_pattern="$( sed 's/\\/\\\\/g' <<EOF
${_FOO_next}
EOF
)"
# We have an assignation to parse
_key="${_FOO%=${_FOO_next_pattern}}"
# We must have a key, assignation without key part are ignored.
# If need, you can output error message in the else branch.
if [ "${_key}" ] ; then
# Triming space and newlines
_key="${_key## }"
_key="${_key##${_NL}}"
_key="${_key## }"
_quote="\'"
# Test if it is quote, if not quote then try double quote
[ "${_FOO_next}" = "${_FOO_next#${_quote}}" ] && _quote="\""
# If not double quote, consider unquoted...
[ "${_FOO_next}" = "${_FOO_next#${_quote}}" ] && _quote=""
# Extracting value part and trim quotes if any
if [ "${_quote}" ] ; then
_FOO_next="${_FOO_next#${_quote}}"
_FOO_next_pattern="${_FOO_next_pattern#${_quote}}"
fi
_value="${_FOO_next}"
if [ "${_quote}" ] ; then
_FOO_next="${_FOO_next#*[^\\]${_quote}}"
_FOO_next_pattern="${_FOO_next_pattern#*[^\\]${_quote}}"
else
# If the value part is not quoted, we look for the next unescaped space
# as the delimiter for the next key/value pair.
_FOO_next="${_FOO_next#*[^\\] }"
_FOO_next_pattern="${_FOO_next_pattern#*[^\\] }"
fi
_value="${_value%${_quote}${_FOO_next_pattern}}"
# We have parse everything need to set the variable
define "${_key}" "${_value}" "${_quote}"
_FOO="${_FOO_next}"
else
_FOO="${_FOO_next#*[^\\] }"
fi
else
# Nothing more to parse
_FOO=""
fi
done
printf "%s\n" "${_SYSCFG_lan_ifname}"
printf "%s\n" "${_SYSCFG_lan_ipaddr}"
printf "%s\n" "${_SYSCFG_lan_netmask}"
printf "%s\n" "${_SYSCFG_space}"
printf "%s\n" "${_SYSCFG_newline}"
The output is the same with ksh93, bash 2..4, dash :
br1
10.0.0.1 *
255.255.255.0
mypath\ so\'urce\my space
I have\nmany multi\nlines input and /path/to/thi ngs
BASH V2+ and KSH93 compliant :
It's non POSIX compliant since the variable substitution by pattern (/
) is not POSIX. The literal ASCII inference $'\x<hex ASCII code>'
is indeed not POSIX, and the following script can only work with ASCII based UNIX shells (forget EBCDIC...). Anyway, this one can handle newline/tab/multiple spaces in the value part of the variables.
#!/bin/sh
# If you only have a bash 4.x, you can test with compat 3.1 bash
# shopt -s compat31
# Test string. There is a TAB between "input" and "and".
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1 *'
SYSCFG_lan_netmask='255.255.255.0' SYSCFG_space='mypath\\ source\\my space'
SYSCFG_newline='I have
many multi
lines input and /path/to/thi ngs"
#
# For bash 2.0, we can't make inline subsitution of ESC nor NL nor TAB because
# of the following bug :
# FOO="`echo -e \"multi\nline\"`";echo "${FOO//$'\x0a'/$'\x1b'}" ==> multi'line
# Bash 2.0 wrongly include one quote to the output in this case.
# To avoid that, we store ESC and NL in local variable, and it is better
# for readability.
_ESC=$'\x1b'
_NL=$'\x0a'
_TAB=$'\x09'
# Same kind of trouble with the backslash in bash 2.0, the substiution need
# 'double' escape for them in bash 2.0, so we store BKS, test it and double it
# if required.
# However, if used as a variable in pattern or subsitution part, we have then to
# deal with two forms of escaped bakcslash since shells don't "dedouble"/escape
# them for the substitute value, only for the pattern.
_BKS_PATTERN="\\\\"
_BKS="\\"
if [ "${_BKS_PATTERN//\\/X}" != "XX" ] ; then
# Hello bash 2.0
_BKS_PATTERN="\\\\\\\\"
_BKS="\\\\"
fi
# An "env variable" definer that use the read command to parse and define
define() {
IFS=\= read -r _key _value <<EOF
$1
EOF
# Unquotting the _value, adapt as it fit your needs
_value="${_value#\'}"
_value="${_value%\'}"
_value="${_value%\'${_BKS_PATTERN}}"
# Unescape the _key string to trim escaped nl
_key="${_key#${_ESC}}"
_key="${_key%${_ESC}}"
# Unescape the _value string
_value="${_value//${_BKS_PATTERN} / }"
_value="${_value//${_ESC}${_ESC}/${_TAB}}"
_value="${_value//${_ESC}/${_NL}}"
read -d\' -r "_${_key}" <<EOF
${_value}'
EOF
}
unset _SYSCFG_lan_ifname
unset _SYSCFG_lan_ipaddr
unset _SYSCFG_lan_netmask
unset _SYSCFG_space
unset _SYSCFG_newline
# First, we escape the new line with 0x1B
_FOO="${FOO//${_NL}/${_ESC}}"
# Second, escape each tab with double ESC. All tabs.
_FOO="${_FOO//${_TAB}/${_ESC}${_ESC}}"
# Third, escape each space. All space.
_FOO="${_FOO// /${_BKS} }"
# Using the set command to "parse" the variables string
set ${_FOO}
while [ "$1" ] ; do
_key_value="$1"
while [ "$1" ] && [ "${_key_value%\'${_BKS_PATTERN}}" = "${_key_value}" ] ; do
shift
_key_value="${_key_value} $1"
done
define "${_key_value}"
[ "$1" ] && shift
done
printf "%s\n" "${_SYSCFG_lan_ifname}"
printf "%s\n" "${_SYSCFG_lan_ipaddr}"
printf "%s\n" "${_SYSCFG_lan_netmask}"
printf "%s\n" "${_SYSCFG_space}"
printf "%s\n" "${_SYSCFG_newline}"
The output is the same with ksh93, bash 2 and + :
(Note that we use printf
to render the TAb char between "input" and "and".)
br1
10.0.0.1 *
255.255.255.0
mypath\ source\my space
I have
many multi
lines input and /path/to/thi ngs