Loading a while true loop into a variable - bash

I'm having a bit of trouble getting this to work/ knowing if its possible. I'm creating a game using little other than bash, this requires a lot of repeated case statements. I am trying to load all the repeated case statements into a variable, then repeat them when necessary to limit the amount of work it will take to update the shared case statements between different scripts.
Here is what I have:
#!/bin/bash
moo="[m][o][o]) echo 'thank you for following instructions' ;;"
test=$(echo "while true ; do
read -p 'type moo: ' case
case $case in
$moo
*) echo 'type moo please'
esac
done")
"$test"
The problem I run into is:
./case.sh: line 13: $'while true ; do\nread -p \'type moo: \' case\ncase in\n[m][o][o]) echo \'thank you for following instructions\' ;;\n*) echo \'type moo please\' ;;\nesac\ndone': command not found
The information in the moo variable will eventually be in a separate script and will be set by invoking it as a function within that script when I finally get a working model.
It looks like this is a workable idea, I've just reached a loss on how to invoke the variable without it acting up. If anyone has any ideas, I would greatly appreciate it.
Thank you in advance!

It doesn't work because the quotes make the variable expansion be treated as a single word.
But it wouldn't work without quotes, either, because the shell doesn't parse the output of variables for syntax like semicolon and newline. Variable expansion is done after that stage of command parsing. The only processing that's done on expanded variables is word-splitting and wildcard matching.
You need to use eval to perform all command parsing:
eval "$test"
Another problem is that the variable $case is being expanded when you assign the variable test, it's not getting the value being read by read. Since the variable doesn't have a value yet, it's being executed as:
case in ...
and this is invalid syntax. You need to escape the $ so it will be passed through literally.
There's also no need for echo, you can simply assign the string directly.
test="while true ; do
read -p 'type moo: ' case
case \$case in
$moo
*) echo 'type moo please'
esac
done"

Related

Equal/minus sign without a colon in a parameter expasion in bash

I found a snippet like this in a Bash script recently:
$ echo ${A=3}
Now, I know that ${A:=3} would set the variable A if A is "falsy", or ${A:-3} would return 3 if A is "falsy." I have never seen these similar expressions without the colon though, and I cannot find the explanation for these colon-less expressions in the Bash's documentation.
What is going on here?
Actually, the documentation does explain what is going on here, even if burying the lede a bit:
When not performing substring expansion, using the form described below (e.g., ‘:-’), Bash tests for a parameter that is unset or null. Omitting the colon results in a test only for a parameter that is unset. Put another way, if the colon is included, the operator tests for both parameters’ existence and that its value is not null; if the colon is omitted, the operator tests only for existence.
In practice, this means that they behave the same way if the variables are unset:
$ echo ${A=no-colon}
no-colon
$ echo ${B:=with-colon}
with-colon
$ echo $A
no-colon
$ echo $B
with-colon
However, if the variables are set to the empty string, then the behavior is different. The expression with a colon will set the variable and return the value, and the one without will leave the variable as is (i.e., set to the empty string) and return its empty value:
$ A='' ; B=''
$ echo ${A=no-colon}
$ echo ${B:=with-colon}
with-colon
$ echo $A
$ echo $B
with-colon
As stated in the documentation, the same behavior applies to the other "operators" (-, ?, +).
Posting it in the spirit of Can I answer my own question? and because it took a surprisingly long time for me to learn it, even after finding it in code. Maybe making it a bit more explicit, with some examples, can help somebody else out there :)

Delayed expansion of composite variable in Bash

I'm defining a variable as a composition of other variables and some text, and I'm trying to get this variable to not expand its containing variables on the assigning. But I want it to expand when called later. That way I could reuse the same template to print different results as the inner variables keep changing. I'm truing to avoid eval as much as possible as I will be receiving some of the inner variables from third parties, and I do not know what to expect.
My use case, as below, is to have some "calling stack" so I can log all messages with the same format and keep a record of the script, function, and line of the logged message in some format like this: script.sh:this_function:42.
My attempted solution
called.sh:
#!/bin/bash
SCRIPT_NAME="`basename "${BASH_SOURCE[0]}"`"
CURR_STACK="${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${CURR_STACK}"
echo
function _func_1 {
echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${CURR_STACK}"
}
_func_1
So, I intend to get the same results while printing the "${CURR_STACK}" as when printing the previous line.
If there is some built-in or other clever way to log this 'call stack', by all means, let me know! I'll gladly wave my code good-bye, but I'd still like to know how to prevent the variables from expanding right away on the assigning of CURR_STACK, but still keep them able to expand further ahead.
Am I missing some shopt?
What I've tried:
Case 1 (expanding on line 4):
CURR_STACK="${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]}"
CURR_STACK="`echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"`"
CURR_STACK="`echo "\${SCRIPT_NAME}:\${FUNCNAME[0]}:\${LINENO[0]}"`"
called.sh::7 <------------------| These are control lines
called.sh::4 <---------------. .------------| With the results I expect to get.
X
called.sh:_func_1:12 <---´ `-------| Both indicate that the values expanded
called.sh::4 <-------------------------| on line 4 - when CURR_STACK was set.
Case 2 (not expanding at all):
CURR_STACK="\${SCRIPT_NAME}:\${FUNNAME[0]}:\${LINENO[0]}"
CURR_STACK=\${SCRIPT_NAME}:\${FUNCNAME[0]}:\${LINENO[0]}
CURR_STACK="`echo '${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}'`"
called.sh::7
${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]} <-------.----| No expansion at all!...
/
called.sh::12 /
${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]} <----´
Shell variables are store plain inert text(*), not executable code; there isn't really any concept of delayed evaluation here. To make something that does something when used, create a function instead of a variable:
print_curr_stack() {
echo "$(basename "${BASH_SOURCE[1]}"):${FUNCNAME[1]}:${BASH_LINENO[0]}"
}
# ...
echo "We are now at $(print_curr_stack)"
# Or just run it directly:
print_curr_stack
Note: using BASH_SOURCE[1] and FUNCNAME[1] gets info about context the function was run from, rather than where it is in the function itself. But for some reason I'm not clear on, BASH_LINENO[1] gets the wrong info, and BASH_LINENO[0] is what you want.
You could also write it to allow the caller to specify additional text to print:
print_curr_stack() {
echo "$#" "$(basename "${BASH_SOURCE[1]}"):${FUNCNAME[1]}:${BASH_LINENO[0]}"
}
# ...
print_curr_stack "We are now at"
(* There's an exception to what I said about variables just contain inert text: some variables -- like $LINENO, $RANDOM, etc -- are handled specially by the shell itself. But you can't create new ones like this except by modifying the shell itself.)
Are you familiar with eval?
$ a=this; b=is; c=a; d=test;
$ e='echo "$a $b $c $d"';
$ eval $e;
this is a test
$ b='is NOT'; # modify one of the variables
$ eval $e;
this is NOT a test
$ f=$(eval $e); # capture the value of the "eval" statement
$ echo $f;
this is NOT a test

Bash assignment value to variable as command substitution and print value output

I would like to achieve this in Bash: echo $(a=1)and print the value of variable a
I test eval, $$a,{}, $() but none of them work as most of the times either I got literally a=1 or in one case (I don't remember which) it tried to execute the value.
I known that I can do: a=1;echo $a but because I'm little fun one command per line (even if sometimes is getting little complicated) I was wondering if is possible to do this either with echo or with printf
If you know that $a is previously unset, you can do this using the following syntax:
echo ${a:=1}
This, and other types of parameter expansion, are defined in the POSIX shell command language specification.
If you want to assign a numeric value, another option, which doesn't depend on the value previously being unset, would be to use an arithmetic expansion:
echo $(( a = 1 ))
This assigns the value and echoes the number that has been assigned.
It's worth mentioning that what you're trying to do cannot be done in a subshell by design because a child process cannot modify the environment of its parent.

How to use eval to force variable update

I was reading the bash advanced scripting guide (if memory serves me right), and it said something to the extent that eval can be used to force variable updates.
So I tried this:
randomPath="/path/$var/here/" # var is not defined at this point
echo $randomPath
/path//here/
var="is" # initially defining var
eval $randomPath
zsh: no such file or directory: /path//here/
I don't understand the error message, and I'm wondering if I'm using eval properly.
The output I was expecting is:
eval $randomPath
echo $randomPath
/path/is/here
The problem is that $var is already being substituted in randomPath="/path/$var/here/", and because it is blank, randomPath is set to /path//here. You want to use single quotes to prevent the early substitution:
randomPath='/path/$var/here/'
The second problem is that eval x runs x as a command. What you want to do is return the newly evaluated variable as a string:
eval echo $randomPath
You can store it in a variable in the usual way:
randomPath=`eval echo $randomPath`

Shell script variable problem

I'm trying to write a shell script to automate a job for me. But i'm currently stuck.
Here's the problem :
I have a variable named var1 (a decreasing number from 25 to 0
and another variable named
var${var1} and this equals to some string.
then when i try to call var${var1} in anywhere in script via echo it fails.
I have tried $[var$var1], ${var$var} and many others but everytime it fails and gives the value of var1 or says operand expected error.
Thanks for your help
It's probably better if you use an array, but you can use indirection:
var25="some string"
var1=25
indirect_var="var$var1"
echo ${!indirect_var} # echoes "some string"
There's only one round of variable expansion, so you can't do it directly. You could use eval:
eval echo \${var$var1}
A better solution is to use an array:
i=5
var[$i]='foo'
echo ${var[$i]}
It sounds like you need bash variable indirection. Take a look at the link below.
http://mywiki.wooledge.org/BashFAQ/006

Resources