Exclamation mark and hash within curly braces in bash - bash

I'm trying to understand a bash script and I'm having trouble with the following line:
result=${!#}
I found part of the solution (! within ${}) here:
If the first character of parameter is an exclamation point (!), it introduces a level of variable indirection. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion.
Another part of the solution (# within ${}) is here:
The length in characters of the expanded value of parameter is substituted. If parameter is ‘’ or ‘#’, the value substituted is the number of positional parameters. If parameter is an array name subscripted by ‘’ or ‘#’, the value substituted is the number of elements in the array. If parameter is an indexed array name subscripted by a negative number, that number is interpreted as relative to one greater than the maximum index of parameter, so negative indices count back from the end of the array, and an index of -1 references the last element.
But I don't know how these two are combined into result. What does this line do?

${#} is the number of arguments in the current shell/function:
$ set -- a b c
$ echo ${#}
3
The ! performs indirect parameter expansion, so the value of ${#} is used as the name of the parameter to expand.
$ echo ${!#} # same as echo ${3}
c
In short, ${!#} expands to the value of the last argument.
In the absence of such bash extensions, one might simply write a loop like
for result; do :; done # instead of result=${!#}
which would iterate over the positional arguments, setting result to each in turn, leaving result with the value of the last one once the loop completes.

Related

Understanding the final `echo` in IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; in order to access current cursor height

I've been reading through the parameter expansion portion of the GNU bash manual guide and I can't seem to understand what is going on in the final echo command if the titular script (IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[};). I understand that the sequence of commands is trying to get the current cursor row by correctly setting the IFS to ; which is the separator of the output of the escape sequence \E[6n (tangentially I don't completely understand why this needs a pre-pended $-sign but maybe that's in the manual somewhere).
What I don't get is, directly from the manual,
${parameter#word}
${parameter##word}
The word is expanded to produce a pattern and matched according to the rules described below (see Pattern Matching). If the pattern matches the beginning of the expanded value of parameter, then the result of the expansion is the expanded value of parameter with the shortest matching pattern (the ‘#’ case) or the longest matching pattern (the ‘##’ case) deleted. If parameter is ‘#’ or ‘’, the pattern removal operation is applied to each positional parameter in turn, and the expansion is the resultant list. If parameter is an array variable subscripted with ‘#’ or ‘’, the pattern removal operation is applied to each member of the array in turn, and the expansion is the resultant list.
what setting word to *[ is doing exactly. The manual seems to say that * is special and used for more of a string or array-style expansion where positional parameters are needed but ROW shouldn't be a string or an array, or if it is it only has the 1 element so why even use *? Also what is the [ for? The manual says that word will be expanded to produce a pattern but what would *[ expand to exactly? (Especially when ROW will just be a number and have no [ characters to match with???)
Thanks in advance!
What I don't get is, [...]
what setting word to *[ is doing exactly.
$ IFS=';' read -sdR -p $'\E[6n' ROW COL
$ declare -p ROW
declare -- ROW=$'\E[24'
It removes \E[ from $ROW.
The terminal responds to \E[6n with cursor position in this format (source):
\E[<row>;<column>R
With IFS set to ; and line delimiter R, ROW is assigned \E[<row>; and ${ROW#*[} is for taking the row number only.
(Especially when ROW will just be a number and have no [ characters to match with???)
This is wrong. Getting the cursor position returns an escape sequence not just the positions that's why you need to remove the ESC and [.
(IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#$'\E'[};)
this also works and it's perhaps more precise as it removes the expected \E not just anything.

Understanding Bash Script argument expressions

With the help of a number of web searches, SO questions and answers, and trial-and-error I have written the following script to send attachments to an email.
attachments=""
subject=""
args=( "$#" ) # Copy arguments
recipient="${#: -1}" # Last argument
unset "args[${#args[#]}-1]" # Remove last argument
for i in "${args[#]}"; do # Remaining Arguments
attachments="$attachments -a $i"
subject="$subject $i"
done
eval "echo 'See Attached …' | mail -r 'Fred <fred#example.net>' $attachments -s \"Attached: $subject\" $recipient"
It works perfectly using something like
send.sh file1 file2 file3 recipient#example.com
I have omitted some of the refinements in the above code, such as error checking, but the whole thing works as planned.
I have no trouble with the process, and I have good programming skills. However I find that Bash scripting is like medieval Latin to me, and I an having a hard time understanding the four expressions which I have commented.
The idea is that I pop the last argument, which is supposed to be the recipient, and loop through the remaining arguments which will be attached files.
Can anybody detail the meanings of the expressions $#, ${#: -1}, ${args[#]}, and args[${#args[#]}-1], and explain what the hash is doing in the last expression?
No doubt the script could stand some improvement, but I only trying to understand what is happening so far.
It's all in bash manual shell parameter expansion and some in bash special parameters. So:
Can anybody detail the meanings of the expressions $#
From the manual, important parts:
$#
($#) Expands to the positional parameters, starting from one. [...]
if not within double quotes, these words are subject to word splitting. In contexts where word splitting is not performed, this expands to a single word with each positional parameter separated by a space. When the expansion occurs within double quotes, and word splitting is performed, each parameter expands to a separate word. That is, "$#" is equivalent to "$1" "$2" ...
So "$#" is equal to "$1" "$2" "$3" ... for each parameter passed. Word splitting is that thing that when a variable is not quoted, it splits argument on spaces, like: a="arg1 arg2 arg3"; f $a runs f with 3 arguments.
${#: -1}
From shell parameter expansion:
${parameter:offset}
${parameter:offset:length}
It expands to up to length characters of the value of parameter starting at the character specified by offset [...]
If parameter is ‘#’, the result is length positional parameters beginning at offset. A negative offset is taken relative to one greater than the greatest positional parameter, so an offset of -1 evaluates to the last positional parameter.
So ${#: -1} is the last positional argument passed to the script. The additional space is there because ${parameter:-word} means something different.
${args[#]}
From bash manual arrays:
Any element of an array may be referenced using ${name[subscript]}. The braces are required to avoid conflicts with the shell’s filename expansion operators. If the subscript is ‘#’ or ‘*’, the word expands to all members of the array name.
${args[#]} is equal to ${args[1]} ${args[2]} ${args[3]}. Note that without quotes word splitting is performed. In your code you have for i in "${args[#]}" - words are preserved.
args[${#args[#]}-1]
From bash manual shell parameter expansion:
${#parameter}
If parameter is an array name subscripted by ‘*’ or ‘#’, the value substituted is the number of elements in the array.
So ${#args[#]} expands to the count of elements in an array. The count of elements -1 is the index of last element. So args[${#args[#]}-1] is args[<the index of last array element>]. The unset "args[${#args[#]}-1]" is used to remove last array element.
explain what the hash is doing in the last expression?
The hash is there to trigger proper variable expansion.
what ( "$#" ) is doing.
From manual:
Arrays are assigned to using compound assignments of the form
name=(value1 value2 … )
The var=("$#") creates an array var with the copy of positional parameters properly expanded with words preserved.
Everything is explained somewhere in the Bash Manual
$# in Special Parameters
Expands to the positional parameters, starting from one. In contexts where word splitting is performed, this expands each positional parameter to a separate word; if not within double quotes, these words are subject to word splitting.
${#: -1} in Shell Parameter Expansion
${parameter:offset}
${parameter:offset:length}
... If parameter is ‘#’, the result is length positional parameters beginning at offset. A negative offset is taken relative to one greater than the greatest positional parameter, so an offset of -1 evaluates to the last positional parameter.
${args[#]} in Arrays
Any element of an array may be referenced using ${name[subscript]}. The braces are required to avoid conflicts with the shell’s filename expansion operators. If the subscript is ‘#’ or ‘*’, the word expands to all members of the array name.
args[${#args[#]}-1] also in Arrays:
${#name[subscript]} expands to the length of ${name[subscript]}. If subscript is ‘#’ or ‘*’, the expansion is the number of elements in the array.

Reading arguments in shell script

Very simple question, but I have very little knowledge of shell script. What does the following command mean, when reading the arguments that were passed to the script?
TXT=${1,,}
It converts a variable to lower case.
Example:
$ var="Hello World"
$ echo ${var,,}
hello world
In your case, $1 refers to the first argument passed to your shell script, so TXT=${1,,} converts the first argument to lower case and stores it in another variable called TXT.
Type man bash and you will find the following detailed explanation of this expression:
${parameter,,pattern}
Case modification. This expansion modifies the case of alphabetic char-
acters in parameter. The pattern is expanded to produce a pattern just
as in pathname expansion. The ^ operator converts lowercase letters
matching pattern to uppercase; the , operator converts matching upper-
case letters to lowercase. The ^^ and ,, expansions convert each
matched character in the expanded value; the ^ and , expansions match
and convert only the first character in the expanded value. If pattern
is omitted, it is treated like a ?, which matches every character. If
parameter is # or *, the case modification operation is applied to each
positional parameter in turn, and the expansion is the resultant list.
If parameter is an array variable subscripted with # or *, the case mod-
ification operation is applied to each member of the array in turn, and
the expansion is the resultant list.

shell script, loop array, why use ${array[#]}

what is the "#" meaning,
code:
#!/bin/bash
array[0]=1
array[1]=2
array[2]=3
array[3]=4
array[4]=5
#work
for num in ${array[#]}
do
echo $num
done
#not work
for num in array
do
echo $num
done
In the loop, why use ${array[#]} not $array, thx
This is the syntax defined by the language. This is just how it works. Read about it in man bash, search for the section titled "Arrays". Here's the relevant part:
Any element of an array may be referenced using ${name[subscript]}.
The braces are required to avoid conflicts with pathname expansion. If
subscript is # or * , the word expands to all members of name. These
subscripts differ only when the word appears within double quotes. If
the word is double-quoted, ${name[*]} expands to a single word with the
value of each array member separated by the first character of the IFS
special variable, and ${name[#]} expands each element of name to a sep‐
arate word. When there are no array members, ${name[#]} expands to
nothing. If the double-quoted expansion occurs within a word, the
expansion of the first parameter is joined with the beginning part of
the original word, and the expansion of the last parameter is joined
with the last part of the original word. This is analogous to the
expansion of the special parameters * and # (see Special Parameters
above).
$array doesn't work like an array. It more nearly acts like ${array[0]}.
$array evaluates to the first element in the array. You must use ${array[#]} (or more properly, "${array[#]}") to indicate each element of the array in turn, separated by the first character of $IFS.

Get the value before decimal point

I am writing a Bash script. I have a variable in it which is a float value. For example:
x=2099.2
I need the value before decimal point, i.e, only 2099.
You can use the following shell parameter expansion:
${x%%.*}
This removes everything from the first dot.
See it live:
$ v=203.4
$ echo ${v%%.*}
203
$ v=2.3.4
$ echo ${v%%.*}
2
From Bash Reference Manual → 3.5.3 Shell Parameter Expansion:
${parameter%%word}
The word is expanded to produce a pattern just as in filename expansion. If the pattern matches a trailing portion of the expanded value of parameter, then the result of the expansion is the value of parameter with the longest matching pattern deleted. If parameter is ‘#’ or ‘’, the pattern removal operation is applied to each positional parameter in turn, and the expansion is the resultant list. If parameter is an array variable subscripted with ‘#’ or ‘’, the pattern removal operation is applied to each member of the array in turn, and the expansion is the resultant list.

Resources