0

I understand that in Bourne shell and derivates, IFS= sets IFS to null, so the shell won't perform any field splitting.

I recall reading about:

  • echo "$*"
  • IFS= echo "$*"

When I ran both commands, once in CentOS and once in WSL (uname -o = GNU/Linux only so I don't know the distribution) I only got empty lines.

What does the above test command mean and is it really a good case to learn about IFS (if not, what simple, two lines, test would you suggest)?

  • 1
    This is not a good test case. First, because in an interactive shell there generally aren't any arguments, so "$*" expands to nothing, and second because using IFS=something doesn't affect how that command's arguments (like "$*") are expanded, it only takes effect on the command as it runs, and echo isn't affected by IFS. – Gordon Davisson Mar 19 '21 at 05:58
  • @GordonDavisson much thanks ! What you wrote is in my opinion the perfect first part for an answer, if you just add one more passage about a simple way about how to test default IFS versus null IFS than this would make a great answer. I have even edited the question to make it fitted for such an answer. – variable_expander Mar 19 '21 at 06:05
  • Furthermore @GordonDavisson comments can be deleted automatically with time and it would be a shame for such a beautiful didactic explanation to go to waste. – variable_expander Mar 19 '21 at 06:06
  • 2
    Note that in the Bourne shell, "$*" joins the positional parameters with SPC regardless of the value of $IFS. The joining with the first character of $IFS is a Korn shell invention. – Stéphane Chazelas Mar 19 '21 at 07:17
  • Hello @StéphaneChazelas ! SPC? I am not sure what that means; Googling SPC bash didn't give me a clear interpretation. – variable_expander Mar 19 '21 at 07:23
  • 1
    SPC is a name for the U+0020 character. Also known as space (though there's a plethora of spacing characters, so using a code name like SPC for the most common one makes it potentially less ambiguous). That's usually the character sent by your terminal or terminal emulator when you press the "space bar" key on English keyboards in English locales at least. – Stéphane Chazelas Mar 19 '21 at 07:30

1 Answers1

5

Before I answer, here's a warning: changing IFS tends to have really confusing effects. There are a few places where it's relatively safe and has well-defined effects (like setting it for the read command, as in IFS= read -r line and IFS=, read -r field1 field2 field3), but trying to understand its behavior in general is, in my opinion, more work than it's worth. Also, if you do change it, either isolate it to a particular command by doing the assignment as a prefix to the command (again, as in IFS= read -r line) or by setting it back to normal as soon as possible after whatever you need to change it for.

Now to the question: these aren't good examples of IFS effects, first because $* expands to the current argument list, and interactive shells don't generally have arguments, so it's going to expand to the empty string. You can change that with the set command if you want. Here's an example:

$ echo "$*"

$ set -- "foo bar" "baz/quux" $ echo "$*" foo bar baz/quux

Second, because changing IFS as a prefix to a particular command (as in IFS= echo "$*") only affects the command as it runs, not while the shell is expanding its arguments (which happens before the command itself runs), and echo itself isn't affected by IFS (it always sticks its arguments together with spaces, no matter what IFS is set to). For example:

$ IFS= echo "$*"    # No effect, because IFS is only set for echo, which ignores it
foo bar baz/quux

Contrast that with setting IFS for the shell itself:

$ saveIFS=$IFS      # Save the normal IFS, so we can set it back later
$ IFS=              # Set IFS for the entire shell
$ echo "$*"         # Now we'll see an effect
foo barbaz/quux
$ IFS='+-*/'        # Let's try another value
$ echo "$*"         # Now the arguments get merged, first char of IFS beween them
foo bar+baz/quux
$ echo $*           # See below
foo bar baz quux
$ printf '%s\n' $*  # See below
foo bar
baz
quux
$ IFS=$saveIFS      # Set it back to normal, to avoid trouble later

So what happened with echo $* when IFS was "+-*/"? Well, IFS is used both to merge and split arguments, so just like with echo "$*", the $* bit expanded to foo bar+baz/quux, but then since it wasn't double-quoted it then immediately got split into words (using all of the characters in IFS), so it got split into three arguments to echo: "foo bar" "baz", and "quux", and echo then stuck those all together with spaces in between. Essentially the same thing happens with the printf command, except the format string makes it print each following argument on a separate line, so you can see that "foo bar" is just one argument.

BTW, some people use the saveIFS approach like I used here, some prefer to unset IFS afterward, which makes the shell act as if IFS was back to its normal space-tab-newline value. Both work, but not together. If you unset IFS, and then later try to save & restore it, it'll come out set to the empty string, and things will be weird. You could also explicitly reset it with IFS=$' \t\n', but that's not portable to all shells (and will cause really weird effects in shells that don't support $' '). Sigh.

EDIT: See this question for more options & discussion of how to save & restore the value of IFS.

BTW2, in most situations you don't want the arguments either merged or split, so what you want is "$@". Unlike $*, $@ doesn't merge the arguments, and with double-quotes it doesn't split them either (or expand them as filename wildcards) -- it just passes them straight through. But don't use args="$@", because that actually does merge them like "$*" (or maybe with " " instead of the first char of IFS, depending on the shell) (it has to merge them to save them in a plain variable). Use args=("$@") instead, to save them as an array.

  • 1
    You make it sound like "$@" is the same as "$*" in a scalar assignment. It is not. The bash shell would not use the first character of $IFS as delimiter with "$@", but space (always). – Kusalananda Mar 19 '21 at 09:43
  • 1
    Another way to temporarily "save and restore" IFS: IFS=_$IFS (do stuff) IFS=${IFS#?}. (This sets the first character of IFS to an _, and then removes it). – Kusalananda Mar 19 '21 at 09:48
  • @Kusalananda Hah, I didn't know that about args="$@". From a quick test, bash and ksh use "", but zsh and dash use the first char of IFS. Fortunately, this isn't really something you should do, so not an incompatibility I'm going to worry about. – Gordon Davisson Mar 19 '21 at 10:05
  • Yeah, using "$@" in a scalar context doesn't really make much sense. That's where you use "$*" after all. – Kusalananda Mar 19 '21 at 10:07
  • @Kusalananda, doesn't that underscore trick still end up with a set but empty IFS if the initial IFS was unset? – ilkkachu Mar 19 '21 at 10:15
  • @ilkkachu So it is. Thanks for the heads-up about that. The "saving the value in a variable and then restoring from it"-variation suffers from the same issue though. – Kusalananda Mar 19 '21 at 10:19
  • 1
    Luckily (/s), you can always do something like unset savedIFS; [ "${IFS+set}" ] && savedIFS=$IFS;, then do your thing, and then unset IFS; [ "${savedIFS+set}" ] && IFS=$savedIFS; ... – ilkkachu Mar 19 '21 at 10:19
  • @Kusalananda, yep... that's the big problem with an unset IFS... doesn't really fit well with the usual expansions treating unset variables as empty. – ilkkachu Mar 19 '21 at 10:21