Bash: "command not found" on simple variable assignment - bash

Here's a simple version of my script which displays the failure:
#!/bin/bash
${something:="false"}
${something_else:="blahblah"}
${name:="file.ext"}
echo ${something}
echo ${something_else}
echo ${name}
When I echo the variables, I get the values I put in, but it also emits an error. What am I doing wrong?
Output:
./test.sh: line 3: blahblah: command not found
./test.sh: line 4: file.ext: command not found
false
blahblah
file.ext
The first two lines are being emitted to stderr, while the next three are being output to stdout.
My platform is fedora 15, bash version 4.2.10.

You can add colon:
: ${something:="false"}
: ${something_else:="blahblah"}
: ${name:="file.ext"}
The trick with a ":" (no-operation command) is that, nothing gets executated, but parameters gets expanded. Personally I don't like this syntax, because for people not knowing this trick the code is difficult to understand.
You can use this as an alternative:
something=${something:-"default value"}
or longer, more portable (but IMHO more readable):
[ "$something" ] || something="default value"

Putting a variable on a line by itself will execute the command stored in the variable. That an assignment is being performed at the same time is incidental.
In short, don't do that.
echo ${something:="false"}
echo ${something_else:="blahblah"}
echo ${name:="file.ext"}

It's simply
variable_name=value
If you use $(variable_name:=value} bash substitutes the variable_name if it is set otherwise it uses the default you specified.

Related

Bash function with parameters

I am trying to understand how to work with functions (that receive a argument) in bash. I did not get my code to work, the following code exemplifies my difficulties:
#!/bin/bash
fruit_code () {
if [[ "$1" == "apple" ]]; then
code=1
else
code=0
fi
return $code
}
for var in apple orange banana apple; do
code=fruit_code $var
echo $code
done
The shell (bash) complains saying:
apple: command not found
orange: command not found
banana: command not found
apple: command not found
So it seems the passing of parameters is not working propperly. I can not see where I am going wrong. Any help is very much appreciated. Why does it not work? What changes shoyld I do to make it work? Wish to thank you all in advance.
Kind regards
Miguel
The first problem is that if you want to execute a command and capture its output to a variable, you need to use command substitution:
code=$(fruit_code "$var")
The second problem is that your function doesn't write anything to standard output; it returns an exit status. That is automatically assigned to $? after you call the function. Use this instead:
fruit_code "$var"
echo $?
Finally, the convention in shell is for a 0 exit status to indicate success and a non-zero value failure. In this case, your function "succeeds" if the argument is not apple.
You have a syntax error in this line:
code=fruit_code $var
A syntax like this:
foo=bar quux
means that you want to run command quux in an environment where variable foo has value bar.
In your case, quux is $var, so it will take the value of $var and try to run it as a command. That's why you are getting errors saying apple, orange, etc. are not commands.
I think what you actually want is to run fruit_code $var and store the output in variable code, right?
In that case you can use this:
code=$(fruit_code $var)
or this:
code=`fruit_code $var`
I prefer the former because it looks clearer to me and allows easy nesting.
Update: Returning strings in functions
Also, as noted by chepner on his answer and as you also noticed in a comment below, you are trying to use return for returning that string value as the result of the function, but return can only be used for returning numeric values as the result of the function (success / error / etc.) so you try to work it around by using numeric values that represent the strings you want to return.
That's overcomplex.
If you want to return a string, use echo instead and you can use $() or `` as explained above to capture the string returned by your function.
So, putting all this together, your code would look like this:
#!/bin/bash
fruit_code () {
echo $1
}
for var in apple orange banana apple; do
code=$(fruit_code $var)
echo $code
done

How to grab value back from external script in bash?

I'm sure I'm missing something stupid. I want to pass a full path variable to a perl script, where I do some work on it and then pass it back. So I have:
echo "Backing up: $f ";
$write_file="$(perl /home/spider/web/foo.com/public_html/gen-path.cgi $f)";
echo "WRITE TO: $write_file \n";
However, this gives me:
Backing up: /home/spider/web/foo.com/public_html/websites-uk/uk/q/u
backup-files-all.sh: line 7: =backup-uk-q-u.tar.gz: command not found
WRITE TO: \n
I can't work out why its not saving the output into $write_file. I must be missing something (bash isn't my prefered language, which is why I'm passing to Perl as I'm a lot more fluent in that :))
Unless your variable write_file already exists, the command $write_file="something" will translate to ="something"(1).
When setting a variable, leave off the $ - you only need it if you want the value of the variable.
In other words, what you need is (note no semicolons needed):
write_file="$(perl /home/spider/web/foo.com/public_html/gen-path.cgi $f)"
(1) It can be even hairier if it is set to something. For example, the code:
write_file=xyzzy
$write_file="something"
will result in something being placed into a variable called xyzzy, not write_file :-)

Get the contents of an expanded expression given to eval through Bash internals

I'm writing some shell functions that allow to print stack traces when errors occur. For this I'm using the BASH_LINENO array which contain the line number for each frame. Then I retrieve the line from the file using BASH_SOURCE array and a subprocess like line="$(tail -n+$lineno "$file" | head -n1)".
Anyway, it works well, except when an error occur within an eval. The problem is that the line number corresponds to the line after the expression given to eval has been expanded. Therefore, when I retrieve the line with head and tail, obviously it's now the wrong one, or it's not a line at all (lineno is superior to the number of lines in the file).
So I wonder how I could get the actual expanded line. I looked at the variables provided by Bash, but none seems to help in this case.
Example, script1.sh:
#!/usr/bin/env bash
eval "$(./script2.sh)"
script2.sh:
#!/usr/bin/env bash
echo
echo
echo
echo false
When I hit the false line when executing script1.sh, the line number I get is 4, and the file source I get is script1.sh, so it's wrong.
When the line is out of the file, I could detect it, and print the first previous eval line instead, but it's very hacky and I'm sure there are a few different cases to handle. And if the line is within the file, then I cannot even know if it's the right one or not.
eval is hell :'(
Ideally, the BASH_COMMAND would be an array as well, and I could retrieve the commands from it instead of reading the files.
Another idea I just have would be to force the user to pipe the result of the expression into a command that will compress it on one line. Any ideas how, or programs to do that? A simple join on ";" seems to naive (again, lots of edge cases).
P.S.: sorry for the title, I have difficulty giving a meaningful title to this one :/
Eventually I found a workaround: by overriding the eval command with my own function, I was able to change the way I print the stack trace for errors happening in eval statements.
eval() {
# pre eval logic
command eval "$#"
# post eval logic
}
Anyway, please don't use eval, or if you do, use only one line arguments:
# GOOD: "easy" to deal with
for i in ...; do
eval "$(some command)"
done
# BAD: this will mess up your line numbers
eval "$(for i in ...; do
some command $i
done)"

Loading a while true loop into a variable

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"

bash associative key with a Variable key

Im trying to create an array and then get the value of a key using the following commands:
declare -A email_addresses
mail_address=(["dev"]="dev.com" ["sandbox"]="sandbox.com")
env=$(#command to get env) # result is "sandbox"
echo ${email_address[$env]}
However it keeps throwing this error at me : -bash: "hsandbox": syntax error: operand expected (error token is ""sandbox"")
Im not sure how to get past this. If I do echo $env it returns "sandbox" and not ""sandbox"" so Im not sure what seems to be the issue.
Fix your "command to get env" to not be emitting literal quotes in its output. Barring that:
# strip leading and trailing quotes from env
env=${env%'"'}; env=${env#'"'}
echo "${email_address[$env]}"
Python-Audience-Friendly Explanation
To explain this in a manner that makes sense to folks who know Python (since that's where most of the OP's rep comes from):
echo "$foo" in shell behaves like the Python command print str(foo), not the Python command print repr(foo).
Consider the following REPL session:
>>> mail_address = { "dev": "dev.com", "sandbox": "sandbox.com" }
>>> env = getSomething()
>>> print str(env)
"dev"
>>> print mail_address[env]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: '"dev"'
>>> print repr(env)
'"dev"'
You've got the exact same problem: Your dictionary contains dev as its literal contents, but your key's literal contents are "dev".
Avoiding Confusion In The Future: Unambiguously Printing Shell Variables
If you want to print a variable's contents in shell in a way that's unambiguous (in the same respect in which print repr(env) is unambiguous in Python), echo is the wrong tool for the job. Consider instead one of the following:
$ declare -p env ## caveat: doesn't work for all non-printable characters
declare -- env="\"dev\""
$ printf 'env=%q\n' "$env" ## caveat: doesn't work for non-string datatypes
env=\"dev\"
An Aside: Why You Should Always Quote Arguments To echo (Or Not Use It)
While it looks innocuous, the code
echo $foo
actually has surprisingly complicated behavior. Consider the following:
foo=$'\thello\tworld\t*\n\\text'
That's the bash equivalent to the following Python:
foo='\thello\tworld\t*\n\\text'
Now, let's see what happens if you actually use echo to print it with echo $foo, if you have a default value for IFS and your shell is bash:
The first tab disappears altogether
The other tabs are replaced by spaces
The newline literal is replaced by a space
The * is replaced by a list of files in the current directory.
That is to say, the behavior in bash of echo $foo is equivalent to the following Python:
import itertools, glob
foo='\thello\tworld\t*\n\\text'
print ' '.join(itertools.chain(*[ glob.glob(s) for s in foo.split() ]))
By contrast, consider:
echo "$foo"
In that case, you'll get the expected behavior... in bash.
Why "in bash"? Because the POSIX standard for echo doesn't specify behavior when any backslash literal is included in the text. echo could do literally anything in this circumstance and still be POSIX-compliant, and BSD-style implementations will behave differently than XSI-style ones do.

Resources