Saturday, October 29, 2022

[SOLVED] Portably setting environment variables from a shell script

Issue

I have this shell script that I would like to modify to ensure it runs correctly in a Bash environment.

Here's the script:

#!/bin/zsh

# Some constants. The first two will become env variables.
UPDATE_DNS_API_HOST="https://example.com"
UPDATE_DNS_API_URL="$UPDATE_DNS_API_HOST/my_end_point"
CURRENT_PUBLIC_IP=$(curl -s "$UPDATE_DNS_API_URL" | grep -o '".*"' | tr -d '"')

if [[ $PUBLIC_IP == $CURRENT_PUBLIC_IP ]]; then
  echo "Current IP: "$CURRENT_PUBLIC_IP" already set."
else
  response=$(curl -s -H "Content-Type: application/json" \
             --data "$CURRENT_PUBLIC_IP" "$UPDATE_DNS_API_URL")
  echo $response
  export PUBLIC_IP="$CURRENT_PUBLIC_IP"
fi

Here are my questions:

  • Should I change the first line to #!/bin/bash
  • It's unclear when variables need quotes and when they don't, especially in conditional statements. Can you point me to some resources here?
  • I've seen variations in conditionals regarding single bracket vs double bracket. Which one should I be using?
  • After running the script, $PUBLIC_IP does not appear to be set. Is there a different way I should be setting the env variable?

Any other feedback is welcome.


Solution

One pertinent thing to keep in mind here is that UNIX processes can modify the environment variable for themselves and future children they start -- not their parents, without that parent process directly participating.


If your intent is to set a variable in the enclosing shell, one fairly common way to do this is to emit shell commands on stdout. This means that anything that isn't a shell command should be moved to stderr (which is appropriate practice anyhow, since stderr is specified as appropriate for informational text and status content).

This version does require bash, as opposed to /bin/sh, but uses printf '%q' to ensure that it's able to generate variable names in an eval-safe manner that all ksh derivatives (ksh, bash, zsh) should be able to read.

#!/bin/bash
# note that while this runs with bash, ksh and zsh will also be able to eval its output
# ...POSIX sh too, when there aren't nonprintable characters causing $''-style quoting
# ...to be used.

# usage: emit_cmd varname ...
#
# emit code that defines a variable when evaluated on stdout
emit_cmd() {
  for varname; do
    printf 'export %q=%q; ' "$varname" "${!varname}"
  done
}

# Some constants. The first two will become env variables.
UPDATE_DNS_API_HOST="https://example.com"
UPDATE_DNS_API_URL="$UPDATE_DNS_API_HOST/my_end_point"

# print definitions of those variables to stdout
emit_cmd UPDATE_DNS_API_HOST UPDATE_DNS_API_URL

CURRENT_PUBLIC_IP=$(curl -s "$UPDATE_DNS_API_URL" | grep -o '".*"' | tr -d '"')

if [[ $PUBLIC_IP = $CURRENT_PUBLIC_IP ]]; then
  echo "Current IP: $CURRENT_PUBLIC_IP already set." >&2
else
  response=$(curl -s -H "Content-Type: application/json" \
             --data "$CURRENT_PUBLIC_IP" "$UPDATE_DNS_API_URL")
  echo "$response" >&2
  PUBLIC_IP="$CURRENT_PUBLIC_IP" emit_cmd PUBLIC_IP
fi

If this script is saved under the name ip-lookup, the variables it defines can be imported into the current shell with:

eval "$(ip-lookup)"

Using this convention keeps compatibility with existing UNIX tools such as ssh-agent which need to modify environment variables.


Note that I'm keeping the existing conventions with respect to variable names, but if you have the opportunity, you should switch to lower-case names to comply with relevant POSIX convention.



Answered By - Charles Duffy
Answer Checked By - Mildred Charles (WPSolving Admin)