exit code of function call ignored if stored in local variable - bash

Below is the sample scenario :-
There is a sample function defined that is echoing some text, as well as setting some exit code using return. There is another script that is calling this function. Below is the simplified code :-
~/playground/octagon/bucket/test/sample $
pwd
/Users/mogli/playground/octagon/bucket/test/sample
~/playground/octagon/bucket/test/sample $
cat functions.sh
myfunc() {
echo "This is output $1"
return 3
}
~/playground/octagon/bucket/test/sample $
cat example.sh
. functions.sh
function example(){
myfunc_out=$(myfunc $1); myfunc_rc=$?
echo "myfunc_out is: $myfunc_out"
echo "myfunc_rc is: $myfunc_rc"
}
example $1
~/playground/octagon/bucket/test/sample $
sh example.sh 44
myfunc_out is: This is output 44
myfunc_rc is: 3
Now, if I use local for variables that are used to store function return value and exit code in example.sh, then exit code is not coming correctly. Please find below modified example.sh :-
~/playground/octagon/bucket/test/sample $
cat example.sh
. functions.sh
function example(){
local myfunc_out=$(myfunc $1); local myfunc_rc=$?
echo "myfunc_out is: $myfunc_out"
echo "myfunc_rc is: $myfunc_rc"
}
example $1
~/playground/octagon/bucket/test/sample $
sh example.sh 44
myfunc_out is: This is output 44
myfunc_rc is: 0

When you write:
var=$(cmd)
the output of cmd is assigned to var (without word splitting) and $? is set to the value returned by cmd.
When you write:
var=$(cmd1) cmd2
cmd1 is executed and its output (without word splitting) is assigned to the variable var in the environment of cmd2, which is then executed. $? is set to the value returned by cmd2.
When you write:
cmd1 var=$(cmd2)
cmd2 is executed, the string var=output of cmd2 is subject to word splitting and passed as argument(s) to cmd1, and $? is set to the value returned by cmd1. (In almost all cases you want to supress the word splitting and instead write cmd1 var="$(cmd2)", which will guarantee that only one argument is passed.)
local is a command, and local myfunc_out=$(myfunc $1) is of the 3rd form (with a caveat), so it sets $? to the value returned by local. Note that if the output of myfunc $1 contains whitespace, word splitting does not take place. To quote the manpage: Assignment statements may also appear as arguments to the alias, declare, typeset, export, readonly, and local builtin commands, so the string counts as a variable assignment and word splitting is not done.
In short, local has an exit value, and it is being used to set $?
Instead of making the assignment an argument to local, you could use:
local myfunc_out myfunc_rc
myfunc_out="$(myfunc $1)"; myfunc_rc=$?
Note that the double quotes are not strictly necessary here, as word splitting does not take place in an assignment, but using them is definitely good practice.

Related

Parameter expansion with replacement, avoid additional variable

I'm trying to join input $* which is one parameter consisting of all the parameters added together.
This works.
#!/bin/bash
foo() {
params="${*}"
echo "${params//[[:space:]]/-}"
}
foo 1 2 3 4
1-2-3-4
However, is it possible to skip the assignment of variable?
"${"${*}"//[[:space:]]/-}"
I'm getting bad substitution error.
I can also do
: "${*}"
echo "${_//[[:space:]]/-}"
But it feels hacky.
One option could be to set bash's internal field separator, IFS, to - locally and just echo "$*":
foo() {
local IFS=$'-'
echo "$*"
}
To answer your question, you can do global pattern substitutions on the positional parameters like this:
${*//pat/sub}
${#//pat/sub}
And also arrays like this:
${arr[*]//pat/sub}
${arr[#]//pat/sub}
This won’t join the parameters, but substitute inside them.
Setting IFS to dash adds a dash in between each parameter for echo "$*", or p=$*, but won’t replace anything inside a parameter.
Eg:
$ set -- aa bb 'cc cc'
$ IFS=-
$ echo "$*"
aa-bb-cc cc
To remove all whitespace, including inside a parameter, you can combine them:
IFS=-
echo "${*//[[:space:]]/-}"
Or just assign to a name first, like you were doing:
no_spaces=$*
echo "${no_spaces//[[:space:]]/-}"

Can IFS be changed locally in a Bash function?

I have a function that needs to change IFS for its logic:
my_func() {
oldIFS=$IFS; IFS=.; var="$1"; arr=($var); IFS=$oldIFS
# more logic here
}
Can I declare IFS as local IFS inside the function so that I don't need to worry about backing its current value and restore later?
It appears to work as you desire.
#!/bin/bash
changeIFSlocal() {
local IFS=.
echo "During local: |$IFS|"
}
changeIFSglobal() {
IFS=.
echo "During global: |$IFS|"
}
echo "Before: |$IFS|"
changeIFSlocal
echo "After local: |$IFS|"
changeIFSglobal
echo "After global: |$IFS|"
This prints:
Before: |
|
During local: |.|
After local: |
|
During global: |.|
After global: |.|
Yes it can be defined!
As long as you define it local, setting of the value in the function does not affect the global IFS value. See the difference between the snippets below
addNumbers () {
local IFS='+'
printf "%s\n" "$(( $* ))"
}
when called in command-line as,
addNumbers 1 2 3 4 5 100
115
and doing
nos=(1 2 3 4 5 100)
echo "${nos[*]}"
from the command line. The hexdump on the above echo output wouldn't show the IFS value defined in the function
echo "${nos[*]}" | hexdump -c
0000000 1 2 3 4 5 1 0 0 \n
000000e
See in one of my answers, how I've used the localized IFS to do arithmetic - How can I add numbers in a bash script
I got confused because I changed the value of IFS to : inside the function (without using local) and then tried to display the value of IFS with this command, after calling the function:
echo $IFS
which displayed an empty line that made me feel the function wasn't changing IFS. After posting the question, I realized that word splitting was at play and I should have used
echo "$IFS"
or
printf '%s\n' "$IFS"
or, even better
set | grep -w IFS=
to accurately display the IFS value.
Coming back to the main topic of local variables, yes, any variable can be declared as local inside a function to limit the scope, except for variables that have been declared readonly (with readonly or declare -r builtin commands). This includes Bash internal variables like BASH_VERSINFO etc.
From help local:
local: local [option] name[=value] ...
Define local variables.
Create a local variable called NAME, and give it VALUE. OPTION can
be any option accepted by `declare'.
Local variables can only be used within a function; they are visible
only to the function where they are defined and its children.
Exit Status:
Returns success unless an invalid option is supplied, a variable
assignment error occurs, or the shell is not executing a function.
You can designate IFS as a local variable; the local version is still used as the field separator string.
Sometimes it is useful to run a function in a completely isolated environment, where no changes are permanent. (For example, if the function needs to change shell options.) This can be achieved by making the function run in a subshell; just change the {} in the function definition to ():
f() (
shopt -s nullglob
IFS=.
# Commands to run in local environment
)

What means PREFIX=${1:-daily} in shell script [duplicate]

This question already has answers here:
Usage of :- (colon dash) in bash
(2 answers)
Closed 5 years ago.
What means these declarations in bash script:
PREFIX=${1:-daily}
PROFILE=${2:-backup}
The key here is to understand what Parameter-Expansion ${PARAMETER:-WORD} syntax means here,
${PARAMETER:-WORD}
If the parameter PARAMETER is unset (never was defined) or null (empty), this one
expands to WORD, otherwise it expands to the value of PARAMETER, as if it
just was ${PARAMETER}
In your case the ${PARAMETER} being the positional arguments passed to a function or script,
Use the below script for a better understanding,
myTestFunction() {
PREFIX=${1:-daily}
PROFILE=${2:-backup}
printf "PREFIX value %s PROFILE value %s\n" "$PREFIX" "$PROFILE"
}
myTestFunction "some" "junk"
myTestFunction
which produces a result as
$ bash script.sh
PREFIX value some PROFILE value junk
PREFIX value daily PROFILE value backup
Also see the expanded debugger version of the script as
$ bash -x script.sh
+ myTestFunction some junk
+ PREFIX=some
+ PROFILE=junk
+ printf 'PREFIX value %s PROFILE value %s\n' some junk
PREFIX value some PROFILE value junk
+ myTestFunction
+ PREFIX=daily
+ PROFILE=backup
+ printf 'PREFIX value %s PROFILE value %s\n' daily backup
PREFIX value daily PROFILE value backup
how shell substitutes the values to the variables when $1 or $2 is not passed.
The syntax is generally used when by default you want to configure a variable with a certain value at the same time make it dynamically configurable also.
Assigns value of first argument to PREFIX variable if this first argument exist, "daily" if it does not.
It means, assign the first argument (if present), else daily to variable PREFIX and assign the second argument (if present), else backup to variable PROFILE
eg:
$ cat file.sh
#!/bin/bash
PREFIX=${1:-daily}
PROFILE=${2:-backup}
echo $PREFIX
echo $PROFILE
For the following command line args given, output would be like:
$ ./file.sh
daily
backup
$ ./file.sh abc
abc
backup
$ ./file.sh abc xyz
abc
xyz

How to parse a string into variables?

I know how to parse a string into variables in the manner of this SO question, e.g.
ABCDE-123456
becomes:
var1=ABCDE
var2=123456
via, say, cut. I can do that in one script, no problem.
But I have a few dozen scripts which parse strings/arguments all in the same fashion (same arguments & variables, i.e. same parsing strategy).
And sometimes I need to make a change or add a variable to the parsing mechanism.
Of course, I could go through every one of my dozens of scripts and change the parsing manually (even if just copy & paste), but that would be tedious and more error-prone to bugs/mistakes.
Is there a modular way to do parse strings/arguments as such?
I thought of writing a script which parses the string/args into variables and then exports, but the export command does not work form child-to-parent, (only vice-versa).
Something like this might work:
parse_it () {
SEP=${SEP--}
string=$1
names=${#:2}
IFS="$SEP" read $names <<< "$string"
}
$ parse_it ABCDE-123456 var1 var2
$ echo "$var1"
ABCDE
$ echo "$var2"
123456
$ SEP=: parse_it "foo:bar:baz" id1 id2 id3
$ echo $id2
bar
The first argument is the string to parse, the remaining arguments are names of variables that get passed to read as the variables to set. (Not quoting $names here is intentional, as we will let the shell split the string into multiple words, one per variable. Valid variable names consist of only _, letters, and numbers, so there are no worries about undesired word splitting or pathname generation by not quoting $names). The function assumes the string uses a single separator of "-", which can be overridden via the environment.
For more complex parsing, you may want to use a custom regular expression (bash 4 or later required for the -g flag to declare):
parse_it () {
reg_ex=$1
string=$2
shift 2
[[ $string =~ $reg_ex ]] || return
i=1
for name; do
declare -g "$name=${BASH_REMATCH[i++]}"
done
}
$ parse_it '(.*)-(.*):(.*)' "abc-123:xyz" id1 id2 id3
$ echo "$id2"
123
I think what you really want is to write your function in one script and include it in all of your other scripts.
You can include other shell scripts by the source or . command.
For example, you can define your parse function in parseString.sh
function parseString {
...
}
And then in any of your other script, do
source parseString.sh
# now we can call parseString function
parseString abcde-12345

Unable to set second to last command line argument to variable

Regardless of the number of arguments passed to my script, I would like for the second to the last argument to always represent a specific variable in my code.
Executing the program I'd type something like this:
sh myprogram.sh -a arg_a -b arg_b special specific
test=("${3}")
echo $test
The results will show 'special'. So using that same idea if I try this (since I won't know that number of arguments):
secondToLastArg=$(($#-1))
echo $secondToLastArg
The results will show '3'. How do I dynamically assign the second to last argument?
You need a bit of math to get the number you want ($(($#-1))), then use indirection (${!n}) to get the actual argument.
$ set -- a b c
$ echo $#
a b c
$ n=$(($#-1))
$ echo $n
2
$ echo ${!n}
b
$
Indirection (${!n}) tells bash to use the value of n as the name of the variable to use ($2, in this case).
You can use $# as array & array chopping methods:
echo ${#:$(($#-1)):1}
It means, use 1 element starting from $(($#-1))...
If some old versions of shells do not support ${array:start:length} syntax but support only ${array:start} syntax, use below hack:
echo ${#:$(($#-1))} | { read x y ; echo $x; } # OR
read x unused <<< `echo ${#:$(($#-1))}`

Resources