2

I just wanted to confirm here since I've only tested in dash shell, but do loop variables collide with variables in the outer scope in shell scripts in general? For example

#! /bin/sh

i='1 2 3'
a='a b c'

for i in $a; do
  echo "$i"
done

echo "$i"

This outputs:

a
b
c
c

which makes sense to me. That is, it seems to indicate that I'm right that loop variables will collide (they share the same namespace as the outer scope). I want to know because if I'm using an older-style shell that doesn't have the local command, I want to be sure to unset loop variables I use in functions. The texts I've read cover unset, but don't seem to cover this case.

Am I right?

5
  • In the way your script is written it's ( the loop ) not forked in a sub-shell -- So yes the name spaces will collide. Commented May 27, 2021 at 18:25
  • @Zak Thanks! How would i make the above example not collide and still print to the parent shell stdout? Commented May 27, 2021 at 18:49
  • 1
    BTW, note that for i in $a is itself an antipattern. The correct way to store lists of things is with an array-type variable. That is, a=( a b c ), then for i in "${a[@]}"; that way you can iterate over lists of strings even when those individual strings can contain spaces or be interpreted as globs. Commented May 27, 2021 at 19:22
  • 1
    For example, let's say you had a='Hello[world] Goodbye' -- if there's a file named Helloo or Hellod in the directory where the script is run, those filenames would be substituted into $i instead of the literal Hello[world] string. Whereas a=( 'Hello[world]' 'Goodbye' ) and for i in "${a[@]}" will assign exactly Hello[world] no matter what filenames exist. Commented May 27, 2021 at 19:30
  • @CharlesDuffy That's only true for shells that support arrays. Consider ksh88 and older versions of Bourne-Style shells. In this case, it is not an antipattern. I don't want to use arrays here. Commented May 27, 2021 at 19:45

2 Answers 2

2

You to avoid namespace issues .. You can fork your script and put the loop inside that fork ..

#! /bin/sh

i='1 2 3'
a='a b c'

function_to_fork(){
  for i in $a; do
     echo "$i"
  done
}

(function_to_fork)

echo "$i"
Sign up to request clarification or add additional context in comments.

4 Comments

@CharlesDuffy That's true. A subprocess would wait for the function to finish. Alternatively, we could capture the PID with ?! and use wait before running echo "$i"
Correct, foo & wait $! is (almost) effectively equivalent to (foo)
@CharlesDuffy I see your point .. But is wait necessary if we use (function_to_fork) instead?
Strike that .. Your comment was hidden .. Noted, and edited ..
1

First: Yes, in all POSIX-compliant shells, variables are global by default, and loops do not have their own scope.

To prevent variables you use from escaping to global context, encapsulate the usage in a function with a local declaration, as follows:

i='1 2 3'
a='a b c'

yourfunc() {
  local i             # <- here, we make i function-local
  for i in $a; do     # aside: don't use unquoted expansions like this
     echo "$i"
  done
}

yourfunc
echo "$i"

...and $i is no longer overwritten.


local is not part of the POSIX sh specification, but it's such a widely-honored extension that even ash and dash provide it.

3 Comments

Thank you, but I am supporting older versions of Bourne-style shells where local is unavailable. Instead, I would use unset in this instance. The option of running the process in the background as @Zak suggested will work for me though.
yourfunc & is a very bad idea. Really do use (yourfunc) instead.
I'll bite. Why do you think it is a bad idea?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.