Why doesn't bash recognize the case option with a hyphen/dash? - bash

I'm trying to use a case statement to determine if I have a legal command. It looks something like this:
function commandTest {
case $1 in
–score) echo "something";;
*) echo "unknown";;
esac
}
Now if I use the function like this, it doesn't work, because case doesn't recognize the string correctly although it is identical.
$ commandTest "-score"
unknown
What am I doing wrong here?

As posted, your sample code has an en-dash (Unicode U+2013) in front of score, instead of a minus sign (ASCII 0x2D), which is preventing bash from matching the string -score
Switch:
–score) echo "something"
to:
-score) echo "something"

Related

Bash prompt multiple command substitution

I am setting up my bash prompt in .bashrc using the following (simplified) function:
set_prompts() {
PS1="\u#\h in \w "
PS1+="\$(get_git_repo_details)"
PS1+="\n"
PS1+="\$(exit_status_prompt)"
}
Now the exit_status_prompt prints a different coloured prompt character, depending on whether the value of $? is 0 or not.
What I noticed though, is that with the code as above, the colour of the prompt character never updates. However, if I append the output of exit_status_prompt to $PS1 before I append the output of get_git_repo_details, or don't append the output of get_git_repo_details at all, then it does update.
Does anyone know what is causing this? Thanks.
Edit:
exit_status_prompt()
{
if [ $? -ne 0 ]
then
highlight 1 "❯ "
else
highlight 2 "❯ "
fi
}
The highlight function then just uses tput to prepend the string in the second parameter with the colour specified in the first parameter.
You need to call exit_status_prompt before doing anything else in set_prompts, or $? is going to be reset. Presumably, exit_status_prompt uses the exit status of the most recently executed command or assignment.
set_prompts() {
esp=$(exit_status_prompt)
PS1="\u#\h in \w "
PS1+="$(get_git_repo_details)"
PS1+="\n"
PS1+="$esp"
}
I've unescaped the command substitutions, because I assume that you are (and should be) running set_prompts as the first command in PROMPT_COMMAND.

Evaluate variable in if statement

So I have an array like:
al_ap_version=('ap_version' '[[ $data -ne $version ]]')
And the condition gets evaluated inside a loop like:
for alert in alert_list; do
data=$(tail -1 somefile)
condition=$(eval echo \${$alert[1]})
if eval "$condition" ; then
echo SomeAlert
fi
done
Whilst this generally works with many scenarios, if $data returns something like "-/-" or "4.2.9", I get errors as it doesn't seem to like complex strings in the variable.
Obviously I can't enclose the variable in single quotes as it won't expand so I'm after any ideas to expand the $data variable (or indeed the $version var which suffers the same possible fate) in a way that the evaluation can handle?
Ignoring the fact that eval is probably super dangerous to use here (unless the data in somefile is controlled by you and only you), there are a few issues to fix in your example code.
In your for loop, alert_list needs to be $alert_list.
Also, as pointed out by #choroba, you should be using != instead of -ne since your input isn't always an integer.
Finally, while debugging, you can add set -x to the top of your script, or add -x to the end of your shebang line to enable verbose output (helps to determine how bash is expanding your variables).
This works for me:
#!/bin/bash -x
data=2.2
version=1
al_ap_version=('ap_version' '[[ $data != $version ]]')
alert_list='al_ap_version'
for alert in $alert_list; do
condition=$(eval echo \${$alert[1]})
if eval "$condition"; then
echo "alert"
fi
done
You could try a more functional approach, even though bash is only just barely capable of such things. On the whole, it is usually a lot easier to pack an action to be executed into a bash function and refer to it with the name of the function, than to try to maintain the action as a string to be evaluated.
But first, the use of an array of names of arrays is awkward. Let's get rid of it.
It's not clear to me the point of element 0, ap_version, in the array al_ap_version but I suppose it has something to do with error messages. If the order of alert processing isn't important, you could replace the list of names of arrays with a single associative array:
declare -A alert_list
alert_list[ap_version]=... # see below
alert_list[os_dsk]=...
and then process them with:
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
...
done
Having done that, we can get rid of the eval, with its consequent ugly necessity for juggling quotes, by creating a bash function for each alert:
check_ap_version() {
(($version != $1))
}
Edit: It seems that $1 is not necessarily numeric, so it would be better to use a non-numeric comparison, although exact version match might not be what you're after either. So perhaps it would be better to use:
check_ap_version() {
[[ $version != $1 ]]
}
Note the convention that the first argument of the function is the data value.
Now we can insert the name of the function into the alert array, and call it indirectly in the loop:
declare -A alert_list
alert_list[ap_version]=check_ap_version
alert_list[os_dsk]=check_op_dsk
check_alerts() {
local alert_name alert
local data=$(tail -1 somefile)
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
if $alert "$data"; then
signal_alert $alert_name
fi
done
}
If you're prepared to be more disciplined about the function names, you can avoid the associative array, and thereby process the alerts in order. Suppose, for example, that every function has the name check_<alert_name>. Then the above could be:
alert_list=(ap_version os_dsk)
check_alerts() {
local alert_name
local data=$(tail -1 somefile)
for alert_name in $alert_list[#]; do
if check_$alert_name "$data"; then
signal_alert $alert_name
fi
done
}

Can a string be returned from a Bash function without using echo or global variables?

I'm returning to a lot of Bash scripting at my work, and I'm rusty.
Is there a way to return a local value string from a function without making it global or using echo? I want the function to be able to interact with the user via screen, but also pass a return value to a variable without something like export return_value="return string". The printf command seems to respond exactly like echo.
For example:
function myfunc() {
[somecommand] "This appears only on the screen"
echo "Return string"
}
# return_value=$(myfunc)
This appears only on the screen
# echo $return_value
Return string
No. Bash doesn't return anything other than a numeric exit status from a function. Your choices are:
Set a non-local variable inside the function.
Use echo, printf, or similar to provide output. That output can then be assigned outside the function using command substitution.
To make it appear only in screen, you can redirect echo to stderr:
echo "This appears only on the screen" >&2
Obviously, stderr should not be redirected.
A creative use of the eval function, you can also assign values to a parameters location, and effectively to your argument, within the body of a function. This is sometimes termed a "call-by-output" parameter.
foo() {
local input="$1";
# local output=$2; # need to use $2 in scope...
eval "${2}=\"Hello, ${input} World!\""
}
foo "Call by Output" output;
echo $output;

Bash Function is not getting called, unless I echo the return value

In my program I am trying to return a value from a function, the return value is string. Everything works fine(atleast some part), if I echo the value once it is returned, but it is not even calling the function, if I dont return.... Consider the code below....
#!/bin/bash
function get_last_name() {
echo "Get Last Name"
ipath=$1
IFS='/'
set $ipath
for item
do
last=$item
done
echo $last
}
main() {
path='/var/lib/iscsi/ifaces/iface0'
current=$(get_last_name "$path")
echo -n "Current="
echo $current
}
main
It gives me an output like this
OUTPUT
Current=Get Last Name iface0
If I comment the echo $current, then the I am not even seeing the "Get Last Name", which makes to come to conclusion, that it is not even calling the function. Please let me know what mistake I am making. But one thing I am sure, bash is the ugliest language I have ever seen.......
Functions do not have return values in bash. When you write
current=$(get_last_name "$path")
you are not assigning a return value to current. You are capturing the standard output of get_last_name (written using the echo command) and assigning it to current. That's why you don't see "Get last name"; that text does not make it to the terminal, but is stored in current.
Detailed explanation
Let's walk through get_last_name first (with some slight modifications to simplify the explanation):
function get_last_name () {
ipath=$1
local IFS='/'
set $ipath
for item
do
last=$item
done
echo "Get Last Name"
echo $last
}
I added the local command before IFS so that the change is confined to the body of get_last_name, and I moved the first echo to the end to emphasize the similarity between the two echo statements. When get_last_name is called, it processes its single argument (a string containing a file path), then echoes two strings: "Get Last Name" and the final component of the file path. If you were to run execute this function from the command line, it would appear something like this:
$ get_last_name /foo/bar/baz
Get Last Name
baz
The exit code of the function would be the exit code of the last command executed, in this case echo $last. This will be 0 as long as the write succeeds (which it almost certainly will).
Now, we look at the function main, which calls get_last_name:
main() {
path='/var/lib/iscsi/ifaces/iface0'
current=$(get_last_name "$path")
echo -n "Current="
echo $current
}
Just like with get_last_name, main will not have a return value; it will produce an exit code which is the exit code of echo $current. The function begins by calling get_last_name inside a command substitution ($(...)), which will capture all the standard output from get_last_name and treat it as a string.
DIGRESSION
Note the difference between the following:
current=$(get_last_name "$path")
sets the value of current to the accumulated standard output of get_last_name. (Among other things, newlines in the output are replaced with spaces, but the full explanation of how whitespace is handled is a topic for another day). This has nothing to do with return values; remember, the exit code (the closet thing bash has to "return values") is a single integer.
current=get_last_name "$path"
would not even call get_last_name. It would interpret "$path" as the name of a command and try to execute it. That command would have a variable current with the string value "get_last_name" in its environment.
The point being, get_last_name doesn't "return" anything that you can assign to a variable. It has an exit code, and it can write to standard output. The $(...) construct lets you capture that output as a string, which you can then (among other things) assign to a variable.
Back to main
Once the value of current is set to the output generated by get_last_name, we execute
two last echo statements to write to standard output again. The first writes "Current=" without a newline, so that the next echo statement produces text on the same line as the first. The second just echoes the value of current.
When you commented out the last echo of main, you didn't stop get_last_name from being executed (it had already been executed). Rather, you just didn't print the contents of the current variable, where the output of get_last_name was placed rather than on the terminal.

BASH: Assign '&' to variable NOT as string

I wanted to conditionally run a command as a background or foreground process, so I wrote something like this:
test $some_var; bg_suffix=&
long_command $bg_suffix
it doesn't work because bg_suffix is always empty whether it's been assigned or not.
But
test $some_var; bg_suffix="&"
long_command $bg_suffix
doesn't work either because now bg_suffix is interpreted as a string.
Any ideas how to solve this problem? Thanks!
Here is how to do it without using a quote-breaking eval
inBackground () {
t=$1
shift
if $t; then
"$#"&
else
"$#"
fi
}
This lets you do something like:
inBackground false echo '$$'
inBackground true sleep 4
This gets around the problem that all the eval-based solutions have: new and sometimes impossible quoting rules. For example, try to pass the '$$' through eval. Because true and false are not significant to the parser they can be in variables and things will still work.
Of course, if you wanted shell metachars to work (say, you redirect i/o) then eval is better, or you need to define a procedure for the command, and if you define a procedure, you problem is solved:
complicated_command () {
sleep 3
echo replace this with something complex
}
do_background=true
$do_background && (complicated_command&) || complicated_command
How about:
if [[ ${somevar} ]] ; then
long_command &
else
long_command
fi
or, if it is a long command you don't want to have to enter twice:
long_command=insert your big honking command here
if [[ ${somevar} ]] ; then
${long_command} &
else
${long_command}
fi
Just as an aside, I hope you're aware that the command sequence:
test ${condition}; x=2
will set x to 2 regardless of the test results. You may have meant to write:
test ${condition} && x=2
did you try
eval (long_command $bg_suffix)
using bg_suffix="&"
I don't know why I am not able comment, but anyway
test $some_var; bg_suffix="&"
would cause bg_suffix to be set regardless of the result of test.

Resources