Bash Functions: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 66: Line 66:


====Pass an Associative Array by Reference====
====Pass an Associative Array by Reference====
This is an example for a patter where a function returns a complex results (multiple key-value pairs) in a map that is passed as argument.
<syntaxhighlight lang='bash'>
<syntaxhighlight lang='bash'>
</syntaxhighlight>
</syntaxhighlight>

Revision as of 02:25, 27 June 2020

Internal

Defintion

Syntax

[function] function-name() {
    ...
}

The "function" keyword is optional.

If the function declaration is specified on a single line, then the final command of the function must be followed by a semicolon:

function one-liner() { echo "something"; echo "something else"; }

The function definition must precede the first call to it. Function bodies may not be empty.

Arguments

The function does not declare its arguments in the signature. They are available in the function's body as positional parameters $1, $2, etc.

Important: For more than 9 arguments, always refer to arguments using ${}: ${10} not $10. If you use $10, you'll actually get the first argument ($1) with a "0" appended to it.

The number of arguments, including the empty strings specified as "" when the function was invoked, may be obtained with $#.

The shift keyword "shifts" to the left the argument list, discarding $1 and assigning to $1 the value that was previously assigned to $2, to $2 the value that was previously assigned to $3, and so on. shift's exit code is 0 if there were still arguments to shift, 1 otherwise.

The correct way to iterate through a function's arguments, including the empty strings, is:

function my-function() {
    while [ $# -gt 0 ]; do
        [ "$1" = "" ] && echo "got empty string" || echo "got $1"
        shift
    done
}

"Pass-by-Reference" Arguments

All arguments are passed by value to bash functions. However, a pass-by-reference mechanism can be simulated using indirect variables: the name of the shell variables to use is passed to the function and the function assigns a value to the variable indicated by name, using assignment to indirect variables:

function example-function() {

    local pass_by_ref_var_name=$1
    eval "${pass_by_ref_var_name}=B"
}

var1=A
echo ${var1}
example-function var1
echo ${var1}

The example prints:

A
B

Pass an Associative Array by Reference

This is an example for a patter where a function returns a complex results (multiple key-value pairs) in a map that is passed as argument.

Exit Status

A bash function does not return a value, it only allows to set an exit status, which is a numerical value. 0 indicates success and a non-zero value indicates failure. The exit status is declared with the "return" keyword:

 function f() {
    ...
    return 0
 }

If there is no explicit "return" keyword, the function's exit status is the exit status of last executed statement.

The function's caller can retrieve the exist status with $?.

Returning Values

As mentioned above, functions do not return values. A function has an exit status.

stdout, stderr

A function may may send content to stdout or stderr from the body of the function, and that content can be captured by the caller as follows:

 function callee() {
    
     echo "we send this to stdout"
     echo "we send this to stderr" 1>&2
 }
 
 function caller() {
     
     local content
     content=$(callee)
     echo "${content}"
 }

Command substitution demonstrated above ends in the capture of the content that is being sent to stdout into the local variable "content". The content sent to stderr will be sent to the stderr of the executing shell.

Piping the stdout and stderr generated by a function

The stdout and stderr content generated by a function can be piped in-line, as follows (we assume the callee() function has been declared as in the stdout, stderr section):

callee | sed -e 's/ /-/g'

will result in "we-send-this-to-stdout" being generated at stdout.

Note that specifying the "|" operator causes the function to execute in a sub-shell. This can be proven by displaying ${BASHPID} in the caller and in the function. They will have different values.

Function Invocation Mechanics

The parameters are passed by value, and no matter what the function does, internally, it will not influence the parameters in the calling layer.

Function Info

The FUNCNAME Array

A function has a "FUNCNAME" built-in array, which is an array variable containing the names of all shell functions currently in the execution call stack. The element with index 0 is the name of any currently-executing shell function. The bottom-most element is "main". This variable exists only when a shell function is executing. Assignments to FUNCNAME have no effect and return an error status. If FUNCNAME is unset, it loses its special properties, even if it is subsequently reset.

The Name of the Function

The name of the function may be obtained inside the function with:

${FUNCNAME[0]}

This is equivalent with:

${FUNCNAME}

Recommended Style

# Function documentation 
# Returns to stdout: ...
# Returns to stderr: ...
function some-function() {

    local arg1=$1  # arg1 documentation
    local arg2=$2  # arg1 documentation
    ...
}

Executing a Function in Background

Functions can be executed in background with '&' as shown:

 function callee() {
    ...
 }

 function caller() {

     callee &

 }

When a function is invoked in background within the context of a shell, a completely new sub-shell is started and begins to execute the code of function. The parent process and the child process share the same name in the process table. The shell that invoked the function in background shows as parent of the newly created sub-shell:

 501  4058  3941   0  9:07PM ttys004    0:00.01 /bin/bash ./main.sh
 501  4059  4058   0  9:07PM ttys004    0:00.01 /bin/bash ./main.sh

From this moment on, both shells are executing independently, until they exit.

stdout and stderr Handling

After the function was invoked in the background, stdout and stderr of the newly created sub-shell that is executing the function is automatically redirected into the parent shell's stdout and stderr.

If the sub-shell is still executing when the parent shell exits normally or it is killed, the sub-shell will keep executing as long as its code allows it. The the init process (process ID 1) becomes its parent. The controlling terminal of the parent will keep getting sub-shell's stdout and stderr content. If the controlling terminal is terminated, the sub-shell will still keep going, but in this case the output will be discarded.

If it is necessary to keep capturing the sub-shell's process after the parent exits and its controlling terminal is terminated, the function must be invoked as such:

function caller() {

     callee > ./callee.stdout 2> ./callee.stderr & 

}

Both stdout and stderr can be send into the same file with the following syntax:

function caller() {

     callee > ./callee.log 2>&1 & 

}

It is important to specify the redirects before the '&' operator. If they are specified after, no syntax error of any kind is detected, "callee.stdout" and "callee.stderr" will be created but will stay empty.

function caller() {

    #
    # INCORRECT, DOES NOT WORK!
    #
    callee & > ./callee.stdout 2> ./callee.stderr

 }

Also see:

bash Process in Background