How to temporarily save and restore the IFS variable properly?

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.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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).

Method 2

Inside a bash function, you can use local IFS=$'n' or whatever to shadow the global (or parent function’s local) value of IFS while inside the scope of this function. Further assignment to IFS will still be modifying your local version.

In bash,

It is an error to use local when not within a function.

So this doesn’t help if you’re not writing a function, or using a shell without local (or equivalent), but if you are (and you know IFS values you wants at all points until it returns), there is an easy and good solution.

A function doesn’t involve a subshell as long as you define it with
foo(){ ...; } instead of foo() ( ... ).

Method 3

In sufficiently old shells, unset either doesn’t exist at all or is unusably buggy (comments in Autoconf’s source code say that unset IFS may crash the process). Kusalananda’s answer cannot be used with such shells.

If you have to worry about shells this old, your best bet is to set IFS to a space, a tab, and a newline, in that order, as early as possible:

# There is a hard tab between the second pair of single quotes.
IFS=' ''    ''
'

This setting has the same effect as an unset IFS, but it can be safely saved and restored with the second construct from the question:

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

(Double-quoting the right hand side of variable=$othervariable is technically not necessary, but it makes life easier for everyone who might have to read your shell script in the future if you don’t make them remember that.)

Method 4

In Bash, I’d do it this way:

[ -v IFS ] && oldIFS="$IFS" || unset oldIFS

IFS=something
some commands

[ -v oldIFS ] && IFS="$oldIFS" || unset IFS

or this way:

[ "${IFS+set}" ] && oldIFS="$IFS" || unset oldIFS

IFS=something
some commands

[ "${oldIFS+set}" ] && IFS="$oldIFS" || unset IFS

Method 5

copy

The initial goal is to copy a variable (a) to another (b).
Doing a simple b=$a works if a is set (either a “” or a value), but if a is unset, b needs to be unset as well. If not, b will be set to “”.

An unset IFS works differently than a null IFS (in bash):

                             $' tn'      unset         null("")
Split Expansions             default       default       no splitting
join arguments with "$*"     "$1c$2c..."   "$1 $2 ..."   "$1$2"

So, we need two steps, copy the value and unset the copied variable (if needed). A variable copy from a to b could be done in several ways:

if [ -n "${a+set}" ]; then unset b; else b="$a"; fi
[ -n "${a+set}" ] && unset b || b="$a"
[ "${a+set}" ] && unset b || b="$a"

${a+'false'} && b=$a || unset b

Then, for IFS, we can copy it to oldIFS, change the value of IFS as needed, and restore it after use:

${IFS+'false'} && oldIFS=$IFS || unset oldIFS

IFS='new value'

${oldIFS+'false'} && IFS=$oldIFS || unset IFS

function(s)

The only way to improve this is to use a function, and yes, a function would be able to copy two vars:

copyIFS    () { ${IFS+'false'} && oldIFS=$IFS || unset oldIFS; }

provided that the names of the variables to modify are known before writing the function as the function must access such variables at the global scope. No local possible, no use of declare/typeset.

It is not possible in sh to create a function for copyvars var1 var2 (with var1 and var2 variable). That would require the use of named vars.

The restore function (using the swapped variable names) is:

restoreIFS () { ${oldIFS+'false'} && IFS=$oldIFS || unset IFS; }

Defining those two functions, we can do:

copyIFS
IFS='a new value'
restoreIFS

Probably simpler, less prone to mistakes.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x