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.
Related
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.
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.
Consider:
# double quotes make empty variables count as args
emptyvar=""
printf %s%s%sEND $emptyvar a b c
echo ""
printf %s%s%sEND "$emptyvar" a b c
echo ""
echo ""
# but an empty array does not count, even with double quotes
empty=()
printf %s%s%sEND ${empty[#]} a c b
echo ""
printf %s%s%sEND "${empty[#]}" a b c
which outputs:
abcEND
abENDcEND
acbEND
abcEND
Try it online!
I understand the first example.
I understand that in the 2nd example the double quotes are somehow forcing the empty string to be considered an arg, because of how word splitting works -- but I'm hazy on the details.
I think the 3rd example is working similar to the first: it just gets processed as whitespace during word splitting.
And I'm unclear why arrays are treated specially in the 4th case.
I'd like an explanation of what is happening under the hood to understand it better, along with any relevant quotes from man bash (I wasn't able to find anything explaining this behavior, but probably missed it).
man bash:
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 separate word. When there are no array members, ${name[#]} expands to nothing.
Sorry for the lame bash question, but I can't seem to be able to work it out.
I have the following simple case:
I have variable like artifact-1.2.3.zip
I would like to get a sub-string between the hyphen and the last index of the dot (both exclusive).
My bash skill are not too strong. I have the following:
a="artifact-1.2.3.zip"; b="-"; echo ${a:$(( $(expr index "$a" "$b" + 1) - $(expr length "$b") ))}
Producing:
1.2.3.zip
How do I remove the .zip part as well?
The bash man page section titled "Variable Substitution" describes using ${var#pattern}, ${var##pattern}, ${var%pattern}, and ${var%%pattern}.
Assuming that you have a variable called filename, e.g.,
filename="artifact-1.2.3.zip"
then, the following are pattern-based extractions:
% echo "${filename%-*}"
artifact
% echo "${filename##*-}"
1.2.3.zip
Why did I use ## instead of #?
If the filename could possibly contain dashes within, such as:
filename="multiple-part-name-1.2.3.zip"
then compare the two following substitutions:
% echo "${filename#*-}"
part-name-1.2.3.zip
% echo "${filename##*-}"
1.2.3.zip
Once having extracted the version and extension, to isolate the version, use:
% verext="${filename##*-}"
% ver="${verext%.*}"
% ext="${verext##*.}"
% echo $ver
1.2.3
% echo $ext
zip
$ a="artifact-1.2.3.zip"; a="${a#*-}"; echo "${a%.*}"
‘#pattern’ removes pattern so long as it matches the beginning of $a.
The syntax of pattern is similar to that used in filename matching.
In our case,
* is any sequence of characters.
- means a literal dash.
Thus #*- matches everything up to, and including, the first dash.
Thus ${a#*-} expands to whatever $a would expand to,
except that artifact- is removed from the expansion,
leaving us with 1.2.3.zip.
Similarly, ‘%pattern’ removes pattern so long as it matches the end of the expansion.
In our case,
. a literal dot.
* any sequence of characters.
Thus %.* is everything including the last dot up to the end of the string.
Thus if $a expands to 1.2.3.zip,
then ${a%.*} expands to 1.2.3.
Job done.
The man page content for this is as follows (at least on my machine, YMMV):
${parameter#word}
${parameter##word}
The word is expanded to produce a pattern just as in pathname
expansion. If the pattern matches the beginning of the 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.
${parameter%word}
${parameter%%word}
The word is expanded to produce a pattern just as in pathname
expansion. If the pattern matches a trailing portion of the
expanded value of parameter, then the result of the expansion is
the expanded value of parameter with the shortest matching pat-
tern (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.
HTH!
EDIT
Kudos to #x4d for the detailed answer.
Still think people should RTFM though.
If they don't understand the manual,
then post another question.
Using Bash RegEx feature:
>str="artifact-1.2.3.zip"
[[ "$str" =~ -(.*)\.[^.]*$ ]] && echo ${BASH_REMATCH[1]}
I think you can do this:
string=${a="artifact-1.2.3.zip"; b="-"; echo ${a:$(( $(expr index "$a" "$b" + 1) - $(expr length "$b") ))}}
substring=${string:0:4}
The last step removes the last 4 characters from the string. There's some more info on here.
I have a bash for loop like this:
for i in /long/path/filename*; do
echo ${i%filename*}/other/path/c01/{magic};
done
Now I would like to get {magic} replaced by the text matched by *
Maybe like this:
for i in /long/path/filename*; do
A=${i/\/long\/path\/filename/}
echo ${i%filename*}/other/path/c01/${A}
done
for i in /long/path/filename*;
do echo ${i%filename*}/other/path/c01/${i#/long/path/filename};
done
From man bash:
${parameter#word}
${parameter##word}
The word is expanded to produce a pattern just as in pathname
expansion. If the pattern matches the beginning of the 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.