Shell Funtion string variables result blank? - bash

I have small script which have a function asking user input (name of user) and then I have echo function
which is running the function and asking for input,
After that I have echo the $User_name in last line (Users_name_is - )which is set in function but its result is black, I want to use $User_name in further script.
what i am doing wrong ?
#!/bin/sh
funtion_one()
{
read varname
if [ $varname == skull ]; then
echo "Nice to meet you $varname"
#User_name=$varname
else
echo "I dont know you $varname"
fi
User_name=$varname
}
echo Hello, who am I talking to?
while :
do
case $(funtion_one) in
"Nice to meet you skull") break
;;
"I dont know you") $(funtion_one)
;;
esac
done
echo "Users_name_is - $User_name"
I want result Users_name_is - skull

When you do $(funtion_one), you are executing the function inside a subshell, so any variables created cease to exist after the function finishes.
An alternative would be this:
function_one()
{
read varname
if [ "$varname" = skull ]; then
echo "Nice to meet you $varname" >&2
else
echo "I dont know you $varname" >&2
fi
echo "$varname"
}
user_name=$(function_one)
Now user_name exists in the parent shell. The messages are sent to standard error, and the name that has been read is sent to standard output so that it can be captured by the command substitution $().
Alternatively, you can simply execute the function in the parent shell:
# change
echo $(funtion_one)
# to
funtion_one
But then all the variables used inside the function will continue to exist after it has been run.
It seems the function is getting in the way of you achieving what you want. I would restructure your code to something much simpler like this:
while read name; do
if [ "$name" = skull ]; then
echo "Nice to meet you $name"
break
fi
echo "I don't know you $name"
done

There are a lot of ways to structure your code, and it seems like you're trying to do something like:
#!/bin/sh
get_user_name() {
local varname
printf 'Hello, who am I talking to? '
read varname
if test "$varname" = skull; then
echo "Nice to meet you $varname"
User_name=$varname
return 0
else
echo "I dont know you $varname" >&2
return 1
fi
}
unset User_name
while ! get_user_name
do
case "$User_name" in
skull) break
;;
esac
done
echo "Users_name_is - $User_name"
It's perfectly valid to use a function to get the input, but if you want that function to set a variable in the caller you cannot call it as a subshell, and it's easiest if the shell returns a value to indicate success or failure.

Related

How to correctly return values in shell script function for edge cases?

getAnimalFolder() {
local animal=""
if [[ ${ANIMAL} == "lion" ]]; then
animal = "./animals/lion/"
elif [[ ${ANIMAL} == "tiger" ]]; then
animal = "./animals/tiger/"
elif [[ ${ANIMAL} == "cheetah" ]]; then
animal = "./animals/cheetah/"
else
echo "inavalid animal"
exit 1`enter code here`
fi
echo $animal
}
result=$(getAnimalFolder)
cd ../result/age/
If the animal is not lion, tiger or cheetah, the function returns invalid animal and hence gives an error 'No such file or directory', instead I need to do an exit with code = 1. Hence I went for the second option -
if [[ ${ANIMAL} != "lion" && ${ANIMAL} != "tiger" && ${ANIMAL} != "cheetah" ]]; then
echo "Invalid animal"
exit 1
fi
getAnimalFolder() {
local animal=""
if [[ ${ANIMAL} == "lion" ]]; then
animal = "./animals/lion/"
elif [[ ${ANIMAL} == "tiger" ]]; then
animal = "./animals/tiger/"
elif [[ ${ANIMAL} == "cheetah" ]]; then
animal = "./animals/cheetah/"
fi
echo $animal
}
result=$(getAnimalFolder)
cd ../result/age/
This looks like a fix to my problem but if in the future more animals are added, then I need to remember to make changes in 2 places for every new animal added. So is there a better way to do this?
There are a number of problems here; #1 and #3 are the ones that directly address your question.
When a function/command/whatever may need to print both regular output (e.g. the path to an animal directory) and error/status output (e.g. "inavalid animal"), it should send the regular output to standard output (aka stdout aka FD #1, the default), and error/status output to standard error (aka stderr aka FD #2), like this:
echo "Invalid animal" >&2 # This is sent to stderr
Generally, functions should return rather than exiting. If a function does exit, it exits the entire shell, but in this case the function is running in a subshell due to $( ), so it only exits that. Using return avoids this inconsistency.
When a function/command/whatever may fail, you should check its exit status; there are a number of ways to do this:
if result=$(getAnimalFolder); then
: # command succeeded!
else
echo "OMG it failed!" >&2
exit 1
fi
or
result=$(getAnimalFolder)
if [ $? -ne 0 ]; then # $? is the status of the last command
echo "OMG it failed!" >&2
exit 1
fi
or
result=$(getAnimalFolder) || {
echo "OMG it failed!" >&2
exit 1
}
I use the last form a lot, since there are a lot of steps in a script might fail, and having a simple & compact way to include the failure handing code makes the overall script more readable.
In general, functions should take their input as arguments rather than via global variables. So in the function you'd refer to $1 instead of $ANIMAL, and you'd run the function with something like:
result=$(getAnimalFolder "$ANIMAL")
There are also a number of basic syntax errors and bad scripting practices in the script: don't put spaces around the equal sign in assignments; do put double-quotes around variable references; don't use all-caps variable names (to avoid conflicts with the many all-caps variables that have special meanings); do check for errors on cd commands (if they fail, the rest of the script will run in the wrong place); and when comparing a single variable against a bunch of values, use case instead of a bunch of if elseif etc.
shellcheck.net is good at recognizing many of these common mistakes. Strongly recommended.
Here's what I get with all fixes in place:
#!/bin/bash
getAnimalFolder() {
local animalname=$1
local animaldir=""
case "$animalname" in
lion ) animaldir="./animals/lion/" ;;
tiger ) animaldir="./animals/tiger/" ;;
cheetah ) animaldir="./animals/cheetah/" ;;
* )
echo "Invalid animal: $animalname" >&2
return 1 ;;
esac
echo "$animaldir"
}
read -p "Give me an animal: " animalname
result=$(getAnimalFolder "$animalname") || {
exit 1 # Appropriate error message has already been printed
}
cd "../$result/age/" || {
echo "Error changing directory to ../$result/age/ -- aborting" >&2
exit 1
}
Put the animals in an array:
#!/bin/bash
animals=(lion tiger cheetah)
getAnimalFolder() {
local i
for i in "${animals[#]}"; do
if [ "$i" == "${1}" ] ; then
animaldir="./animals/${1}"
return 0
fi
done
exit 1
}
read -rp "Give me an animal: " animalname
getAnimalFolder "${animalname}"
echo "Animaldir=${animaldir}"
EDIT:
I did not use the construction result=$(getAnimalFolder), assuming the OP wants to use the new path once. When needed, the function can be changed into
echo "./animals/${1}"
When the function is called with result=$(getAnimalFolder), OP needs to look at the line
cd ../result/age/
Is resulta fixed path or does he want to use the path from the function:
cd ../${result}/age/

bash script to trap all interupts

so i have a simple lab exercise in class. Write and Interrupt/signal trapping program. this program will prompt user to guess the age of a grandmother. The user may guess as many times as possible. nothing will be able to terminate this program execpt when the user enters the right answer.
so my question is this. I have trapped ctrl_c but is there some "trick" or command i can use to trap ALL the interrupts or do i need to just make a statement for each signal i want to trap.
age=88
trap ctrl_c INT
function ctrl_c()
{
echo "**Trapped CTRL-C"
}
while [ 1 ]
do
echo "Please enter Grandmothers age. "
read ageGuess
echo $ageGuess
if [ $ageGuess == $age ]
then
echo "Exiting!"
exit
fi
done
Afaik trap xxx INT should be enough you can read more here and here, I will mention a few things about your script though:
The test command ([) uses = to compare two strings, not ==.
You should double-quote all your variables unless you are sure what will happen if they are not quoted, consider this:
a=a
b=a
# This will work since `$a` and `$b` contains a value
if [ $a = $b ]; then
echo hello
fi
This will fail since $c is empty and the statement will evaluate to: if [ = ]; then
if [ $c = $d ]; then
echo fail
fi
You should use true or : in your while loop:
while :; do
You should usually always use -r in read, and remember the shebang:
#!/bin/bash
age=88
trap ctrl_c INT
ctrl_c() {
echo "**Trapped CTRL-C"
}
while :; do
echo "Please enter Grandmothers age. "
read -r ageGuess
echo "$ageGuess"
if [ "$ageGuess" = "$age" ]; then
echo "Exiting!"
exit
fi
done
Actually the code could now use #!/bin/sh since it POSIX compatible.
Just note that you might want to use printf "%s\n" "$user_input" instead of echo "$user_input"

Check if the parameter of a shell command exists

In my program, I have to check whether a command given as a input by a user exists or not and if it exists, program needs to check if the parameters of that command are correct.
For example:
ls ( is correct)
-al (is correct)
do the watch
and if I do this:
ls (is correct)
-kala (not correct)
don't do the watch.
How I can do this? Here is my script:
while true
do
echo "Insert the command"
read comm
if [ "$(type -t $comm)" != "" ]; then
echo "Insert the parameters of the command ";
read par;
echo "Insert the time of watch";
read time;
if [ $t -le 0 ]; then
echo "Value not correct";
else
clear;
while true
do
echo "$comm"
date
echo ""
$comm $par
sleep $((time))
clear
done
fi;
else
echo "Command not found, retry.";
echo "";
fi
done
You can replace the command invocation with this:
if ! $comm $par; then
exit 1
fi
to make it stop after an error. Also there is already a tool called watch but I think you already know this.

Return exactly one string/value from shell script

I have read qns regarding return output from a function in Stack Overflow. All the post says to use echo
#!/bin/bash
function myown()
{
echo "i dont need this in retval"
echo "Need this alone in retVal"
}
retVal=$(myown)
echo $retVal
o/p:
i dont need this in retval Need this alone in retVal
expected:
Need this alone in retVal
Is there a way to flush the previous output in echo. Or I need to parse all the echoed output to get my return value ? Is there simple way to do this ? Because I may have echos that are useful to debug and echo to return a value.
Echo output to stderr for debugging:
#!/bin/bash
function myown()
{
echo "i dont need this in retval" >&2
echo "Need this alone in retVal"
}
retVal=$(myown)
echo "result: $retVal"
When you run the script, you will see
i dont need this in retval
result: Need this alone in retVal

Can I pass an arbitrary block of commands to a bash function?

I am working on a bash script where I need to conditionally execute some things if a particular file exists. This is happening multiple times, so I abstracted the following function:
function conditional-do {
if [ -f $1 ]
then
echo "Doing stuff"
$2
else
echo "File doesn't exist!"
end
}
Now, when I want to execute this, I do something like:
function exec-stuff {
echo "do some command"
echo "do another command"
}
conditional-do /path/to/file exec-stuff
The problem is, I am bothered that I am defining 2 things: the function of a group of commands to execute, and then invoking my first function.
I would like to pass this block of commands (often 2 or more) directly to "conditional-do" in a clean manner, but I have no idea how this is doable (or if it is even possible)... does anyone have any ideas?
Note, I need it to be a readable solution... otherwise I would rather stick with what I have.
This should be readable to most C programmers:
function file_exists {
if ( [ -e $1 ] ) then
echo "Doing stuff"
else
echo "File $1 doesn't exist"
false
fi
}
file_exists filename && (
echo "Do your stuff..."
)
or the one-liner
file_exists filename && echo "Do your stuff..."
Now, if you really want the code to be run from the function, this is how you can do that:
function file_exists {
if ( [ -e $1 ] ) then
echo "Doing stuff"
shift
$*
else
echo "File $1 doesn't exist"
false
fi
}
file_exists filename echo "Do your stuff..."
I don't like that solution though, because you will eventually end up doing escaping of the command string.
EDIT: Changed "eval $*" to $ *. Eval is not required, actually. As is common with bash scripts, it was written when I had had a couple of beers ;-)
One (possibly-hack) solution is to store the separate functions as separate scripts altogether.
The cannonical answer:
[ -f $filename ] && echo "it has worked!"
or you can wrap it up if you really want to:
function file-exists {
[ "$1" ] && [ -f $1 ]
}
file-exists $filename && echo "It has worked"

Resources