How to pass bash commands as function arguments in bash? - bash

Here is my bash script where I am trying to pass the bash command as function arguments to the function. However, I get an error. I am assuming it is trying to run the command.
./test1.sh: line 19: find /etc -type f > /dev/null: No such file or directory
here is my entire script. why am I getting this error what is the reason behind this?
#!/bin/bash
run_time_cmd() {
cmd="$1"
TIMEFORMAT='%3lR'
exec 3>&1 4>&2
echo "print cmd"
echo $cmd
echo "running cmd = $cmd"
time_val=$( { time $cmd 2>&1; 1>&3- 2>&4-; } 2>&1 ) # Captures time only.
return time_val
}
test_find_time() {
echo 'find test starting'
path="/etc"
cmd="find $path -type f > /dev/null"
echo $cmd
total_time=run_time_cmd "${cmd}"
echo $'Time to access each file average in seconds = ' $total_time
echo 'find test stopped'
find_results=$total_time
}
test_find_time

You never call the function. The line
total_time=run_time_cmd "${cmd}"
is non-sense. In general, a line
X=Y Z
in bash runs the program Z in an environment, which is like the environment of the caller, but the variable X is set to Y.
In your case, you take the whole content of cmd (i.e. the string find /etc -type f > /dev/null) as a single command name (including all the spaces) and try to execute it in an environment, where the variable toatl_time has been set to the string run_time_cmd. Since there is no executable file in your path with the funny name 'find /etc -type f > /dev/null', you get the error message which you mention in your question.
In order to have your function invoked, you have to actually call it, for instance by doing
total_time=$(run_time_cmd "$cmd")

Related

BASH - 'exit 1' failed in loop inside another loop [duplicate]

This question already has answers here:
Exit bash script within while loop
(2 answers)
Closed last month.
The following code doesn't exit at the first exit 1 from the call of error_exit. What am I missing?
#!/bin/bash
THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JINJANG_DIR="$(cd "$THIS_DIR/../.." && pwd)"
DATAS_DIR="$THIS_DIR/datas"
error_exit() {
echo ""
echo "ERROR - Following command opens the file that has raised an error."
echo ""
echo " > open \"$1\""
exit 1
}
cd "$DATAS_DIR"
find . -name 'datas.*' -type f | sort | while read -r datafile
do
localdir="$(dirname $datafile)"
echo " * Testing ''$localdir''."
filename=$(basename "$datafile")
ext=${filename##*.}
if [ "$ext" == "py" ]
then
unsafe="-u"
else
unsafe=""
fi
datas="$DATAS_DIR/$datafile"
find . -name 'template.*' -type f | sort | while read -r template
do
filename=$(basename "$template")
ext=${filename##*.}
template="$DATAS_DIR/$template"
outputfound="$DATAS_DIR/$localdir/output_found.$ext"
cd "$JINJANG_DIR"
python -m src $UNSAFE "$DATA" "$TEMPLATE" "$OUTPUTFOUND" || error_exit "$localdir"
done
cd "$DATAS_DIR"
done
Here is the output I obtain.
ERROR - Following command opens the file that has raised an error.
> open "./html/no-param-1"
* Testing ''./html/no-param-2''.
ERROR - Following command opens the file that has raised an error.
> open "./html/no-param-2"
* Testing ''./latex/no-param-1''.
ERROR - Following command opens the file that has raised an error.
> open "./latex/no-param-1"
* Testing ''./latex/no-param-2''.
ERROR - Following command opens the file that has raised an error.
In my bash environment invoking exit in a subprocess does not abort the parent process, eg:
$ echo "1 2 3" | exit # does not exit my console but instead ...
$ # presents me with the command prompt
In your case you have the pipeline: find | sort | while, so the python || error_exit is being called within a subprocess which in turn means the exit 1 will apply to the subprocess but not the (parent) script.
One solution that insures the (inner) while (and thus the exit 1) is not run in a subprocess:
while read -r template
do
... snip ...
python ... || error_exit
... snip ...
done < <(find . -name 'template.*' -type f | sort)
NOTES:
I'd recommend getting used to this structure as it also addresses another common issue ...
values assigned to variables in a subprocess are not passed 'up' to the parent process
subprocess behavior may differ in other shells
Of course, this same issue applies to the parent/outer while loop so, if the objective is for the exit 1 to apply to the entire script then this same structure will need to be implemented for the parent/outer find | sort | while, too:
while read -r datafile
do
... snip ...
while read -r template
do
... snip ...
python ... || error_exit
done < <(find . -name 'template.*' -type f | sort)
cd "$DATAS_DIR"
done < <(find . -name 'datas.*' -type f | sort)
Additional note copied from GordonDavisson's edit of this answer:
Note that the <( ) construct ("process substitution") is not
available in all shells, or even in bash when it's in sh-compatibility
mode (i.e. when it's invoked as sh or /bin/sh). So be sure to use
an explicit bash shebang (like #!/bin/bash or #!/usr/bin/env bash)
in your script, and don't override it by running the script with the
sh command.

Syntax for inlining return value of call

While you can inline output of a program as parameters
$ echo $(ls)
cpp python bash
or as a temporary file
$ echo <(ls)
/proc/self/fd/63
I wonder how you can inline the return value with a similar syntax, so that it echoes the return-value of ls that it works like this:
$ ls
$ echo $?
0
ls_retval=$(ls >/dev/null 2>&1; echo "$?")
If you want to encapsulate that:
# define a function...
retval_of() { "$#" >/dev/null 2>&1; echo "$?"; }
# and use it
ls_retval=$(retval_of ls)
As for "with a similar syntax", though -- the shell has the syntax that it has; there doesn't exist "retval substitution" (as of bash 4.4, or POSIX sh as standardized in POSIX Issue 7).

Making a copy of command line arguments inside a function

Currently at work on the following version of Bash:
GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)
My current script:
#!/usr/bin/env bash
function main() {
local commands=$#
for command in ${commands[#]} ; do
echo "command arg: $command"
done
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
set -e
main $#
fi
In simple terms, this script will only exec main if it's the script being called, similar to Python's if __name__ == '__main__' convention.
In the main function, I'm simply looping over all the command variables, but quote escaping isn't happening as expected:
$ tests/simple /bin/bash -c 'echo true'
command arg: /bin/bash
command arg: -c
command arg: echo
command arg: true
The last argument here should get parsed by Bash as a single argument, nevertheless it is split into individual words.
What am I doing wrong? I want echo true to show up as a single argument.
You are getting the right output except for the 'echo true' part which is getting word split. You need to use double quotes in your code:
main "$#"
And in the function:
function main() {
local commands=("$#") # need () and double quotes here
for command in "${commands[#]}" ; do
echo "command arg: $command"
done
}
The function gets its own copy of $# and hence you don't really need to make a local copy of it.
With these changes, we get this output:
command arg: /bin/bash
command arg: -c
command arg: echo true
In general, it is not good to store shell commands in a variable. See BashFAQ/050.
See also:
How to copy an array in Bash?
You'll likely want to do something more like this:
function main() {
while [ $# -gt 0 ]
do
echo "$1"
shift
done
}
main /bin/bash -c "echo true"
The key really being $#, which counts the number of command line arguments, (not including the invocation name $0). The environment variable $# is automatically set to the number of command line arguments. If the function/script was called with the following command line:
$ main /bin/bash -c "echo true"
$# would have the value "3" for the arguments: "/bin/bash", "-c", and "echo true". The last one counts as one argument, because they are enclosed within quotes.
The shift command "shifts" all command line arguments one position to the left.
The leftmost argument is lost (main).
Quoting of # passed to main was your issue, but I thought I would mention that you also do not need to assign the value inside main to use it.
You could do the following:
main()
{
for command
do
...
done
}
main "$#"

How can I differentiate "command substitution" from "subshell" inside a script?

I need to differentiate two cases: ( …subshell… ) vs $( …command substitution… )
I already have the following function which differentiates between being run in either a command substitution or a subshell and being run directly in the script.
#!/bin/bash
set -e
function setMyPid() {
myPid="$(bash -c 'echo $PPID')"
}
function echoScriptRunWay() {
local myPid
setMyPid
if [[ $myPid == $$ ]]; then
echo "function run directly in the script"
else
echo "function run from subshell or substitution"
fi
}
echoScriptRunWay
echo "$(echoScriptRunWay)"
( echoScriptRunWay; )
Example output:
function run directly in the script
function run from subshell or substitution
function run from subshell or substitution
Desired output
But I want to update the code so it differentiates between command substitution and subshell. I want it to produce the output:
function run directly in the script
function run from substitution
function run from subshell
P.S. I need to differentiate these cases because Bash has different behavior for the built-in trap command when run in command substitution and in a subshell.
P.P.S. i care about echoScriptRunWay | cat command also. But it's new question for me which i created here.
I don't think one can reliably test if a command is run inside a command substitution.
You could test if stdout differs from the stdout of the main script, and if it does, boldly infer it might have been redirected. For example
samefd() {
# Test if the passed file descriptors share the same inode
perl -MPOSIX -e "exit 1 unless (fstat($1))[1] == (fstat($2))[1]"
}
exec {mainstdout}>&1
whereami() {
if ((BASHPID == $$))
then
echo "In parent shell."
elif samefd 1 $mainstdout
then
echo "In subshell."
else
echo "In command substitution (I guess so)."
fi
}
whereami
(whereami)
echo $(whereami)

Bash variables expansion (possible use of eval) in for-do loop

I am studying the book "Beginning Linux Programming 4th ed" and chapter 2 is about shell programming. I was impressed by the example on page 53, and tried to develop a script to display more on that. Here is my code:
enter code here
#!/bin/bash
var1=10
var2=20
var3=30
var4=40
for i in 1 2 3 4 # This works as intended!
do
x=var$i
y=$(($x))
echo $x = $y # But we can avoid declaring extra parameters x and y, see next line
printf " %s \n" "var$i = $(($x))"
done
for j in 1 2 3 4 #This has problems!
do
psword=PS$j
#eval psval='$'PS$i # Produces the same output as the next line
eval psval='$'$psword
echo '$'$psword = $psval
#echo "\$$psword = $psval" #The same as previous line
#echo $(eval '$'PS${i}) #Futile attempts
#echo PS$i = $(($PS${i}))
#echo PS$i = $(($PS{i}))
done
#I can not make it work as I want : the output I expect is
#PS1 = \[\e]0;\u#\h: \w\a\]${debian_chroot:+($debian_chroot)}\u#\h:\w\$
#PS2 = >
#PS3 =
#PS4 = +
How can I get the intended output? When I run it as it is I only get
PS1 =
PS2 =
PS3 =
PS4 = +
What happened with PS1 and PS2 ?
Why do not I get the same value that I get with
echo $PS1
echo $PS2
echo $PS3
echo $PS4
because that was what I am trying to get.
Shell running a script is always non interactive shell. You may force to run the script in interactive mode using '-i' option:
Try to change:
#!/bin/bash
to:
#!/bin/bash -i
see INVOCATION section in 'man bash' (bash.bashrc is where your PS1 is defined):
When an interactive shell that is not a login shell is started, bash reads and executes commands from
/etc/bash.bashrc and ~/.bashrc, if these files exist. This may be inhibited by using the --norc option. The
--rcfile file option will force bash to read and execute commands from file instead of /etc/bash.bashrc and
~/.bashrc.
When bash is started non-interactively, to run a shell script, for example, it looks for the variable BASH_ENV in
the environment, expands its value if it appears there, and uses the expanded value as the name of a file to read
and execute. Bash behaves as if the following command were executed:
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
but the value of the PATH variable is not used to search for the file name.
you can also read: http://tldp.org/LDP/abs/html/intandnonint.html
simple test:
$ cat > test.sh
echo "PS1: $PS1"
$ ./test.sh
PS1:
$ cat > test.sh
#!/bin/bash -i
echo "PS1: $PS1"
$ ./test.sh
PS1: ${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u#\h\[\033[01;34m\] \w \$\[\033[00m\]
Use indirect expansion:
for j in 0 1 2 3 4; do
psword="PS$j"
echo "$psword = ${!psword}"
done

Resources