[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Re: Fascinating problem with bash



Bob McGowan wrote:
> All this got me to wondering, so I looked at the two links Bob provided.
>  And, I did some tests of my own.
> 
> First, I think there's an error on the SubShell page, in the "example"
> of the difference between a "subshell" and a full "child process", at
> the end.  The author uses double quotes for the subshell, then single
> and double quotes for the child process.  It's the single quotes that
> prevent evaluation of $a, not the "child process" versus "subshell".

I think you misunderstand.  The example is okay.  The single quotes
keep the text verbatim for passing as an argument to the sh -c 'cmd'
or they would have been expanded by the current shell.

  $ a=foo
  $ echo $a
  $ sh -c "echo $a"

Here because double quotes were used the $a was expanded in the string
argument before it was passed to the 'sh' program.  The result was
actually sh -c "echo foo" and the 'sh' program never saw the dollar
sign and would not expand the variable.

Therefore the argument to the subshell needs to be quoted with single
quotes to prevent the expansion.  Then when the subshell interprets
the string it will have a $a and the subshell will expand it.

  $ sh -c 'echo "$a"'

> If you use single quotes in the subshell line, the $a is printed as is:
> 
>   $ (echo 'a is $a in subshell')
>   a is $a in subshell
>   $

Right.  But then the single quotes prevent the $a from being expanded.

> Since the double quotes in the child process example are not needed,
> removing them and replacing the single quotes with double quotes results
> in output with $a replaced by it's value, 1.
> 
>   $ sh -c "echo a is $a in child"
>   a is 1 in child
>   $

Here your example expanded the $a in the parent shell before passing
the string to the subshell.  The subshell did not expand any
variables.

Also if the 'a' variable contained whitespace then because it isn't
quoted the whitespace will be eaten by the shell like a turnip due to
the IFS.  You can verify that by putting something into a that
contains more than one space.

  $ a="one   two"

  $ (echo "a is $a in the subshell")
  a is one   two in the subshell

  $ (echo a is $a in the subshell)
  a is one two in the subshell

See how the spaces were lost in the insufficiently quoted example?

In any case I think the point of the example was to illustrate the
difference between exported variables and unexported variables.
Normally child processes will only have access to exported variables.
But shell sub-shells are a fork and have access to the shell's
internal state including unexported variables.  If you export 's' in
the previous example then the sh -c 'echo "$a"' case will have access
to the value of 'a' but normally doesn't because it isn't exported.

> Getting quoting right in shell scripts is often difficult.  ;-)

Yes!  But it isn't terrible.  There are rules.  After those are known
then everything else follows.

> This is the code used for my testing.  Note I use double quotes only and
> backslashes when I want to "quote" specific single characters to prevent
> evaluation.  The quoting forces the use of 'eval' in the 'while' loop's
> first echo, to force variable substitution to happen when the loop is
> run, otherwise the output would be strings, $$ and $SHLVL, literally.

I didn't quite understand what you are trying to say there.  Sorry.

>   #!/bin/bash
> 
>   SHLVL=1 # I'm using ksh which is setting this to 2, in GUI env.
>   # This also means you may not want to trust the value, in some cases.

For the most part I always ignore SHLVL. (shrug)

>   for n in 1
>   do
>     echo iteration: $n pid1 is $$ SHLVL is $SHLVL

Sure.  I mostly put this line here so that my next comment won't be
confused with having anything to do with the for loop.  :-)

>     echo $n | while read m
>     do

That pipe means that the loop will occur in a child process.  Any
variables set within that loop will evaporate when the child process
exits.  The parent outside the loop will not contain any setting from
the child process.

>       MyVar='while loop'
>       eval echo "iteration: $m and pid2 is \$$ SHLVL is \$SHLVL"

Shouldn't that be:

  eval echo "iteration: $m and pid2 is \$\$ SHLVL is \$SHLVL"

Because otherwise dollar-space is expanded, which isn't a variable so
isn't expanded.  But in principle I think it should be quoted too if
you don't want the $$ to be expanded.

But I don't understand why you are using eval there.  It is equivalent
to this, isn't it?

  echo "iteration: $m and pid2 is $$ SHLVL is $SHLVL"

Because the first pass across the line with the eval is going to
change it from this

  eval echo "iteration: $m and pid2 is \$$ SHLVL is \$SHLVL"

Into this

  echo "iteration: 1 and pid2 is $$ SHLVL is $SHLVL"

And then the second pass across the line will expand those arguments
into something like this, with appropriate number values

  echo "iteration: 1 and pid2 is 5577 SHLVL is 1"

And then the echo will emit that string.  The variables will have been
expanded before the echo is invoked.

>       bash -c "echo parent is $$ I\'m \$$ SHLVL is \$SHLVL"

Again I think the dollar space should be quoted too.

 bash -c "echo parent is $$ I\'m \$\$ SHLVL is \$SHLVL"

>       if [ "$MyVar" ]
>       then
>         echo $MyVar
>       else
>         echo MyVar is empty
>       fi

This is inside the loop, which is in the child process.  The variable
set there is available.  It will print 'while loop'.

>     done | cat # Just to put the loop between two pipes.

The first pipe was enough.  The second pipe isn't useful.  :-)

>     if [ "$MyVar" ]
>     then
>       echo $MyVar
>     else
>       echo MyVar is empty
>     fi

Because this is in the parent process the value set in the child
process evaporated when the child process exited.  The value in the
parent process was never set to anything.

>   done
> 
> The only point where SHLVL, and $$, get 'reset', is in the explicit
> execution of 'bash -c'.

The bash man page says:

       SHLVL  Incremented by one each time an instance of bash is started.

> I believe this suggests modern shells are maintaining the functionality
> of a "subshell", but are running things in the "current" process, for
> reasons of efficiency.

It is a fork(2) of the current shell.  The current process is fork'd
into a child process and the child is handling the loop.

  $ man 2 fork

       fork() creates a new process by duplicating the calling
       process.  The new process, referred to as the child, is an
       exact duplicate of the calling process, referred to as the
       parent, except for the following points:
       ... see the man page for all of the details ...
         I would have liked to have replicated the entire thing here
         but it is quite long and so really you just need to read the
         man page.

Because the child process is a fork of the parent then unexported
variables (private variables) are available.  But that is a one-way
street.  They are available to the child process.  But the parent is
still the parent and when the child exits all information associated
with it evaporates along with the child.  The parent process never
witnesses the information processed in the child.

> Or, I'm completely off my rocker (possible) and not getting it (also
> possible).  If there's a better explanation, I'd like to see it ;)

I am hoping this helped clear up one or two confusing points.

Bob

Attachment: signature.asc
Description: Digital signature


Reply to: