How to Temporarily Save and Restore the IFS Variable

shellvariable

How do I correctly run a few commands with an altered value of the IFS variable (to change the way field splitting works and how "$*" is handled), and then restore the original value of IFS?

I know I can do

(
    IFS='my value here'
    my-commands here
)

to localize the change of IFS to the sub-shell, but I don't really want to start a sub-shell, especially not if I need to change or set the values of variables that needs to be visible outside of the sub-shell.

I know I can use

saved_IFS=$IFS; IFS='my value here'
my-commands here
IFS=$saved_IFS

but that seems to not restore IFS correctly in the case that the original IFS was actually unset.

Looking for answers that are shell agnostic (but POSIX).

Clarification: That last line above means that I'm not interested in a bash-exclusive solution. In fact, the system I'm using most, OpenBSD, does not even come with bash installed at all by default, and bash is not a shell I use for anything much other than to answer questions on this site. It's much more interesting to see solutions that I may use in bash or other POSIX-like shells without making an effort to write non-portable code.

Best Answer

Yes, in the case when IFS is unset, restoring the value from $saved_IFS would actually set the value of IFS (to an empty value).

This would affect the way field splitting of unquoted expansions is done, it would affect field splitting for the read built-in utility, and it would affect the way the positional parameters are combined into a string when using "$*".

With an unset IFS these things would happen as if IFS had the value of a space, a tab character, and a newline character, but with an empty value, there would be no field splitting and the positional parameters would be concatenated into a string with no delimiter when using "$*". So, there's a difference.

To correctly restore IFS, consider setting saved_IFS only if IFS is actually set to something.

unset saved_IFS
[ -n "${IFS+set}" ] && saved_IFS=$IFS

The parameter substitution ${IFS+set} expands to the string set only if IFS is set, even if it is set to an empty string. If IFS is unset, it expands to an empty string, which means that the -n test would be false and saved_IFS would remain unset.

Now, saved_IFS is unset if IFS was initially unset, or it has the value that IFS had, and you can set whatever value you want for IFS and run your code.

When restoring IFS, you do a similar thing:

unset IFS
[ -n "${saved_IFS+set}" ] && { IFS=$saved_IFS; unset saved_IFS; }

The final unset saved_IFS isn't really necessary, but it may be good to clean up old variables from the environment.


An alternative way of doing this, suggested by LL3 in comments (now deleted), relies on prefixing the unset command by :, a built-in utility that does nothing, effectively commenting out the unset, when it's not needed:

saved_IFS=$IFS
${IFS+':'} unset saved_IFS

This sets saved_IFS to the value of $IFS, but then unsets it if IFS was unset.

Then set IFS to your value and run you commands. Then restore with

IFS=$saved_IFS
${saved_IFS+':'} unset IFS

(possibly followed by unset saved_IFS if you want to clean up that variable too).

Note that : must be quoted, as above, or escaped as \:, so that it isn't modified by $IFS containing : (the unquoted parameter substitution invokes field splitting, after all).