Search code examples
linuxbashteebash-function

In bash tee is making function variables local, how do I escape this?


I have stucked with a bash scipt which should write both to stdout and into file. I'm using functions and some variables inside them. Whenever I try to redirect the function to a file and print on the screen with tee I can't use the variables that I used in function, so they become local somehow. Here is simple example:

#!/bin/bash
LOGV=/root/log

function var()
{
echo -e "Please, insert VAR value:\n"
read -re VAR
}
var 2>&1 | tee $LOGV
echo "This is VAR:$VAR"

Output:

[root@testbox ~]# ./var.sh   
Please, insert VAR value:

foo
This is VAR:
[root@testbox ~]#

Thanks in advance!

EDIT: Responding on @Etan Reisner suggestion to use var 2>&1 > >(tee $LOGV)

The only problem of this construction is that log file dosn't receive everything...

[root@testbox~]# ./var.sh
Please, insert VAR value: 

foo 
This is VAR:foo
[root@testbox ~]# cat log 
Please, insert VAR value:

Solution

  • This is a variant of BashFAQ #24.

    var 2>&1 | tee $LOGV
    

    ...like any shell pipeline, has the option to run the function var inside a subprocess -- and, in practice, behaves this way in bash. (The POSIX sh specification leaves the details of which pipeline components, if any, run inside the parent shell undefined).


    Avoiding this is as simple as not using a pipeline.

    var > >(tee "$LOGV") 2>&1
    

    ...uses process substitution (a ksh extension adopted by bash, not present in POSIX sh) to represent the tee subprocess through a filename (in the form /dev/fd/## on modern Linux) which output can be redirected to without moving the function into a pipeline.


    If you want to ensure that tee exits before other commands run, use a lock:

    #!/bin/bash
    logv=/tmp/log
    
    collect_var() {
            echo "value for var:"
            read -re var
    }
    collect_var > >(logv="$logv" flock "$logv" -c 'exec tee "$logv"') 2>&1
    flock "$logv" -c true # wait for tee to exit
    
    echo "This is var: $var"
    

    Incidentally, if you want to run multiple commands with their output being piped in this way, you should invoke the tee only once, and feed into it as appropriate:

    #!/bin/bash
    logv=/tmp/log
    collect_var() { echo "value for var:"; read -re var; }
    
    exec 3> >(logv="$logv" flock "$logv" -c 'exec tee "$logv"') # open output to log
    collect_var >&3 2>&3         # run function, sending stdout/stderr to log
    echo "This is var: $var" >&3 # ...and optionally run other commands the same way
    exec 3>&-                    # close output
    flock "$logv" -c true        # ...and wait for tee to finish flushing and exit.