if statement, that calls a function and compares values to variables - bash

I use i3 and want to control the volume of my sink using the volume keys, that call pactl. Since pactl does not support a parameter for max volume, but is adviced to use with pipewire, the script should get the current volume and compare, if the next volume_increment step would result in a higher value than a certain max_volume. My confusion is now related to an if statement, with the combination of calculating the difference between the volume_increment and max_volume, and compare it to the outcome of the current_volume function. So the volume will be raised, until the certain value, where the increment would result in a higher volume then the maximum value
#!/bin/bash
volume_increment=$1
max_volume=$2
function get_volume() {
pactl get-sink-volume #DEFAULT_SINK# | head -n1 | cut -d/ -f2 | tr -d ' %'
}
if (( get_volume >= $max_volume - $volume_increment )) ; then
pactl set-sink-volume #DEFAULT_SINK# +$volume_increment%
else
pactl set-sink-volume #DEFAULT_SINK# $max_volume%
fi
I've googled a lot and tried several variations of brackets in the if statement. I've managed to get it to work at some intermediate step. But then I've read that (()) brackets are not universal for POSIX shells.
Now I'm very confused and can't separate these various aspects in my head.
Would someone be so kind to explain the logic of calling a function in a statement and compare it to a difference of variables? Preferably for universal POSIX usage, nut bash specific.

Note that in arithmetic context, variables are implicitly expaned. Therefore, ((var)) uses $var automatically.
In your statement
(( get_volume >= $max_volume - $volume_increment ))
the $ is not needed - you could equally well have written max_volume - volume_increment, but you are refering to a variable get_volume and never set it. An unset variable is treated as zero, and therefore get_volume is always 0. Assuming that you can caluclate this value from the function current_volume, you could do a
get_volume=$(current_volume)
or run this function directly inside the arithmetic expression, i.e.
(( $(current_volume) >= $max_volume - $volume_increment ))
Of course, all this requires that you are restricting yourself to integer arithmetic in your script.

Related

(bash) What is the least redundant way to systematically apply changes to an array of variables?

My goal is to check a list of file paths if they end in "/" and remove it if that is the case.
Ideally I would like to change the original FILEPATH variables to reflect this change, and I'd like this to work for a long list without unnecessary redundancy. I tried doing it as a loop, but the changes didn't alter the original variables, it just changed the iterating "EACH_PATH" variable. Can anyone think of a better way to do this?
Here is my code:
FILEPATH1="filepath1/file1"
FILEPATH2="filepath2/file2/"
PATH_ARRAY=(${FILEPATH1} ${FILEPATH2})
echo ${PATH_ARRAY[#]}
for EACH_PATH in ${PATH_ARRAY[#]}
do
if [ "${EACH_PATH:$((${#EACH_PATH}-1)):${#EACH_PATH}}"=="/" ]
then EACH_PATH=${EACH_PATH:0:$((${#EACH_PATH}-1))}
fi
done
edit: I'm happy to do this in a totally different way and scrap the code above, I just want to know the most elegant way to do this.
I'm not entirely clear on the actual goal here, but depending on the situation I can see several possible solutions. The best (if it'll work in the situation) is to dispense with the individual variables, and just use array entries. For example, you could use:
declare -a filepath
filepath[1]="filepath1/file1"
filepath[2]="filepath2/file2/"
for index in "${!filepath[#]}"; do
if [[ "${filepath[index]}" = *?/ ]]; then
filepath[index]="${filepath[index]%/}"
fi
done
...and then use "${filepath[x]}" instead of "$FILEPATHx" throughout. Some notes:
I've used lowercase names. It's generally best to avoid all-caps names, since there are a lot of them with special functions, and accidentally using one of those names can cause trouble.
"${!filepath[#]}" gets a list of the indexes of the array (in this case, "1" "2") rather than their values; this is necessary so we can set the entries rather than just look at them.
I changed the logic of the slash-trimming test -- it uses [[ = ]] to do pattern matching, to see if the entry ends with "/" and has at least one character before that (i.e. it isn't just "/", 'cause you don't want to trim that). Then it uses in the expansion %/ to just trim "/" from the end of the value.
If a numerically-indexed array won't work (and you have at least bash version 4), how about a string-indexed ("associative") array? It's very similar, but use declare -A and use $ on variables in the index (and generally quote them). Something like this:
declare -A filepath
filepath["foo"]="filepath1/file1"
filepath["bar"]="filepath2/file2/"
for index in "${!filepath[#]}"; do
if [[ "${filepath["$index"]}" = *?/ ]]; then
filepath["$index"]="${filepath["$index"]%/}"
fi
done
If you really need separate variables instead of array entries, you might be able to use an array of variable names, and indirect variable references. how this works varies quite a bit between different shells, and can easily be unsafe depending on what's in your data (in this case, specifically what's in path_array). Here's a way to do it in bash:
filepath1="filepath1/file1"
filepath2="filepath2/file2/"
path_array=(filepath1 filepath2)
for varname in "${path_array[#]}"; do
if [[ "${!varname}" = *?/ ]]; then
declare "$varname=${!varname%/}"
fi
done
Using sed
PATH_ARRAY=($(echo ${PATH_ARRAY[#]} | sed 's#\/ ##g;s#/$##g'))
Demo:
$FILEPATH1="filepath1/file1"
$FILEPATH2="filepath2/file2/"
$PATH_ARRAY=(${FILEPATH1} ${FILEPATH2})
$echo ${PATH_ARRAY[#]}
filepath1/file1 filepath2/file2/
$PATH_ARRAY=($(echo ${PATH_ARRAY[#]} | sed 's#\/ ##g;s#/$##g'))
$echo ${PATH_ARRAY[#]}
filepath1/file1 filepath2/file2
$

How to read argument value inside for loop range for shell scripting [duplicate]

I'm working on getting accustomed to shell scripting and ran across a behavior I found interesting and unexplained. In the following code the first for loop will execute correctly but the second will not.
declare letters=(a b c d e f g)
for i in {0..7}; do
echo ${letters[i]}
done
for i in {0..${#letters[*]}}; do
echo ${letters[i]}
done
The second for loop results in the following error:
syntax error: operand expected (error token is "{0..7}")
What confuses me is that ${#letters[*]} is clearly getting evaluated, correctly, to the number 7. But despite this the code fails even though we just saw that the same loop with {0..7} works perfectly fine.
What is the reason for this?
I am running OS X 10.12.2, GNU bash version 3.2.57.
The bracket expansion happens before parameter expansion (see EXPANSIONS in man bash), therefore it works for literals only. In other words, you can't use brace expansion with variables.
You can use a C-style loop:
for ((i=0; i<${#letters[#]}; i++)) ; do
echo ${letters[i]}
done
or an external command like seq:
for i in $(seq 1 ${#letters[#]}) ; do
echo ${letters[i-1]}
done
But you usually don't need the indices, instead one loops over the elements themselves, see #TomFenech's answer below. He also shows another way of getting the list of indices.
Note that it should be {0..6}, not 7.
Brace expansion occurs before parameter expansion, so you can't use a variable as part of a range.
Expand the array into a list of values:
for letter in "${letters[#]}"; do
echo "$letter"
done
Or, expand the indices of the array into a list:
for i in ${!letters[#]}; do
echo "${letters[i]}"
done
As mentioned in the comments (thanks), these two approaches also accommodate sparse arrays; you can't always assume that an array defines a value for every index between 0 and ${#letters[#]}.

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.

Why can a bash function recursively call itself without using local variables?

I am doing scripts in bash. It is said in this site (http://tldp.org/LDP/abs/html/recurnolocvar.html) that "A function may recursively call itself even without use of local variables." but it was not explained why.
There is a sample function involving the fibonacci sequence. He commented on the code that it doesnt need to be local and asked why, but did not answer. A part is shown below:
Fibonacci ()
{
idx=$1 # Doesn't need to be local. Why not?
if [ "$idx" -lt "$MINIDX" ]
then
echo "$idx" # First two terms are 0 1 ... see above.
else
(( --idx )) # j-1
term1=$( Fibonacci $idx ) # Fibo(j-1)
(( --idx )) # j-2
term2=$( Fibonacci $idx ) # Fibo(j-2)
echo $(( term1 + term2 ))
fi
}
The Fibonacci function has an "idx" variable which could have been modified by successive calls because the successive "idx" definitions are not declared local, hence it should affect the previous definitions.
The previous topic on that site (http://tldp.org/LDP/abs/html/localvar.html) demonstrates that if a variable is not declared as local (therefore defaults to global) then changing it would reflect changes in global scope.
Why can a bash function recursively call itself without using local variables?
Because the variables effectively are local.
The command in process substitution ($()) is run by a subshell. Variable values don't propagate back from subshell. So the recursive calls can't affect the parent call.
Commands run in subshell are:
process substitution ($(command))
both sides of a pipeline (command1 | command2)
explicit subshell ((command))
background jobs (command&)
(bash-specific) process redirections (>(command), <(command))
There is no way to propagate any variable values back from these.
It's because he calls the function recursively through a subshell:
term1=$( Fibonacci $idx ) # Fibo(j-1)
I actually find that inefficient For every level of recursion, a process is summoned and could cause overload to the system. It's better to use local variables.

bash while loop with command as part of the expression?

I am trying to read part of a file and stop and a particular line, using bash. I am not very familiar with bash, but I've been reading the manual and various references, and I don't understand why something like the following does not work (but instead produces a syntax error):
while { read -u 4 line } && (test "$line" != "$header_line")
do
echo in loop, line=$line
done
I think I could write a loop that tests a "done" variable, and then do my real tests inside the loop and set "done" appropriately, but I am curious as to 1) why the above does not work, and 2) is there some small correction that would make it work? I still fairly confused about when to use [, (, {, or ((, so perhaps some other combination would work, though I have tried several.
(Note: The "read -u 4 line" works fine when I call it above the loop. I have opened a file on file descriptor 4.)
I think what you want is more like this:
while read -u 4 line && test "$line" != "$header_line"
do
...
done
Braces (the {} characters) are used to separate variables from other parts of a string when whitespace cannot be used. For example, echo "${var}x" will print the value of the variable var followed by an x, but echo "$varx" will print the value of the variable varx.
Brackets (the [] characters) are used as a shortcut for the test program. [ is another name for test, but when test detects that it was called with [ it required a ] as its last argument. The point is clarity.
Parenthesis (the () characters) are used in a number of different situations. They generally start subshells, although not always (I'm not really certain in case #3 here):
Retrieving a single exit code from a series of processes, or a single output stream from a sequence of commands. For example, (echo "Hi" ; echo "Bye") | sed -e "s/Hi/Hello/" will print two lines, "Hello" and "Bye". It is the easiest way to get multiple echo statements to produce a single stream.
Evaluating commands as if they were variables: $(expr 1 + 1) will act like a variable, but will produce the value 2.
Performing math: $((5 * 4 / 3 + 2 % 1)) will evaluate like a variable, but will compute the result of that mathematical expression.
The && operator is a list operator - he seperates two commands and only executes when the first is true, but in this case the first is the while and he is expecting his do stuff. And then he reaches do and the while stuff is already history.
Your intention is to put it into the expression. So you put it together with (). E.g. this a solution with just a small change
while ( read -u 4 line && test "$line" != "$header_line" )

Resources