1

I want my sourced script to run with nounset, pipefail etc. I want to restore the options to their previous values when it returns.

I want an errexit-like behavior inside the sourced script too. It's not exactly errexit because I want the sourced script to return on error, but don't cause the caller to exit.

This is what I came up with, it seems to work for the simple examples I tried.

One problem I see is that this will not work with actual errexit if somebody tries to use it in the sourced script.

Another problem is that apparently nounset will not trigger an ERR trap at all.

trap " err_status=\$? if ! [[ \${FUNCNAME[1]-} ]]; then echo 'Resetting shell options' >&2 trap - RETURN ERR $(shopt -po) return \$err_status else echo 'Returning with status' \$err_status >&2 return \$err_status fi " ERR trap " echo 'Resetting shell options' >&2 trap - RETURN ERR $(shopt -po) " RETURN set -o errtrace -o nounset -o pipefail +o history fail() { echo "About to fail" false echo "This will not print" } echo "Calling" "${1-fail}" echo "After call" 

Results:

$ . demotrap.sh false; echo $? Calling Resetting shell options 1 $ . demotrap.sh true; echo $? Calling After call Resetting shell options 0 $ . demotrap.sh fail; echo $? Calling About to fail Returning with status 1 Resetting shell options 1 
10
  • Why do you need to run this as a sourced script? This sort of state restoring would be way easier if it were an independent script that could just exit. Commented Jul 3 at 17:34
  • I need to set environment variables, so I guess I have to source? I know about the . <( ./script-outputing-shell-statements) hack, but it awkward to use interactively. Commented Jul 3 at 17:40
  • Could you split it into two scripts, one that you source to set variables, and then one that you run normally to do the rest of the work? Commented Jul 3 at 18:56
  • yeah, either . <(foo) or exec "$(foo)". Yes, either is awkward to write by hand, but you can wrap that part in a function (or a sourced script) and do the actual logic in an independent script Commented Jul 3 at 19:04
  • 1
    @JakubBochenski, err, oops, that should have been eval, not exec. eval "$(whatever)". Well, they do mostly the same, but the process substitution <(cmd) is not POSIX, so that wouldn't work if you ever tried that with a more limited shell. eval "$(whatever)" would be more widely supported. Probably doesn't matter much, since people likely use either Bash or zsh as their interactive shell (unless they use something non-POSIX-like, and the neither <() or $() might work). Commented Jul 4 at 19:08

2 Answers 2

2

IMO your desire to source the file that makes changes to the variables is in conflict with the desire to reset those variables back to their original (default) values. You'll have a much cleaner reset back to original values and less complex code if you adopt a parent/child approach. (already suggested in a comment)

When a parent script process exports variables, a child script inherits the exported variables. So the approach I suggest is to have two scripts, one that will source a file with the default values for these variables and invoke a child script. The child script will customize its copy of the variables (either through explicit code in the script, or by sourcing a different file than the parent script used) and then invoke the commands that make use of the customized variables. Afterward the child script exits.

When the child script exits, the parent continues to have the original (default) values in the variables. The parent script can invoke the child again, perhaps with command-line arguments that tell the child script to source another file to customize the variables, and invoke the commands that make use of the customized variables. Then the child script exits again.

With this approach, your script doesn't have to have a routine that carefully wipes the modified variables and re-loads the original values. It happens automatically by virtue of the child script exiting and the parent invoking the child script again.

4
  • I'm not sure I follow the suggested workflow. Say script A is the script from my question. It sets up a service using docker-compose, the exports the ports as environment variables. Another script B wants to use those environment variables to make calls to the service. I'm not sure in your proposal which would be parent and which would be child? Also not there are multiple scripts like B Commented Jul 4 at 11:00
  • In this description, you have one script setting variables with values, and another script using those values to make calls to the service [in the docker container]. You don't seem to visualize the second script inheriting the values by virtue of being spawned as a child process from the first script. I know you've found a solution that satisfies you, but I'm curious about the way you envision the second script "uses" the environment variables set by the first script without a parent/child relationship. And which script does the "restore to previous values", and what causes the restoration. Commented Jul 6 at 6:39
  • If I follow you right then: the "parent" will in most cases be an interactive shell session. This is the biggest reason to restore, since errexit is inconvenient for that use case. As for "why not have A spawn B" there are two reasons. First there are multiple B scripts. Second the B script takes it's own arguments. You could do passthrough of arguments from A to B but it seems brittle to me. I'd rather have A independent of specifics of B except for the few env variables that B consumes Commented Jul 7 at 15:12
  • The multiple B scripts take different arguments, but they would all use the same set of environment variables (only needing different values)? That's VERY unusual. Are you describing a situation where an interactive user wants to invoke diverse scripts/commands, and wants to expand a set of known variables as part of the command-line arguments to those scripts? It's still not clear what you're referring to when you say "errexit" - some kind of automated reset of the values in the variables, based on whether the last script exited with success or error? Commented Jul 9 at 0:42
2

I realized you could use the . <( ./script-outputing-shell-statements) trick inside the sourced script itself.

Now I can set whatever I want inside the subshell and it will be reset on exit (because it's a subshell).

Also I can use regular errexit since it will only exit the subshell.

#!/bin/bash __wrapper() { local -r original_args=("$@") local output err_status output=$( set -euo pipefail # do stuff cat <<SHELL export foo=$(printf '%q' "$foo") SHELL ); err_status=$? if (( err_status )) then echo "$output" return $err_status else if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then echo echo "Script is not sourced, environment variables will not be set." echo $'Please source this script by running:\n\n\t'"source ${BASH_SOURCE[0]} ${original_args[*]}" echo echo $'Paste this into your terminal to set the environment variables:\n' echo "$output" echo else # shellcheck disable=SC1090 . <(echo "$output") fi fi } __wrapper "$@" unset __wrapper 

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.