can't understand bash script : $# etc - bash

I'm looking at this bash script and can't understand.
I can understand $# is all the positional parameters(arguments from shell) and #array[#] means the number of parameters. (let alone its queer syntax..). The rest I don't understand..
#/bin/bash
array=( $# )
len=${#array[#]}
EXTRA_ARGS=${array[#]:2:$len}
EXTRA_ARGS_SLUG=${EXTRA_ARGS// /_}
EDIT : oh, I realized, the third line is taking some range of parameters from the array, and the last line is doing concatenating all the parameters with '_'.

EXTRA_ARGS_SLUG=${EXTRA_ARGS// /_}
replaces space / / by underscore /_

The last two are subclasses of parameter expansion in bash.
${array[#]:2:$len} is array slicing pattern. The syntax is:
${parameter:offset:length}
So it will output array element starting from index 2 to whatever length the variable $len contains.
${EXTRA_ARGS// /_} is replacement pattern. The syntax is:
${parameter//pattern/replacement}
All spaces in parameter EXTRA_ARGS are replaced with _.
To replace only the first occurrence, the pattern ${EXTRA_ARGS/ /_} would be used.

This code is incorrect, because it ignores the reason to use $#: to protect whitespace that occurs in arguments. You should always quote $#. By assigning to EXTRA_ARGS, you lose the ability to distinguish between spaces that are part of an argument, and spaces that separate arguments.
array=( "$#" )
EXTRA_ARGS=( "${array[#]:2:$len}" )
OLD_IFS=$IFS; IFS=_; EXTRA_ARGS_SLUG=${EXTRA_ARGS[*]}; IFS=$OLD_IFS
You don't actually need the array; you can work directly with $#:
EXTRA_ARGS=( "${#:3:$len}" ) # positional parameters are numbered from 1, though

Related

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.

how to pass args to bash functions [duplicate]

This question already has answers here:
Propagate all arguments in a Bash shell script
(12 answers)
Closed 3 years ago.
Let's say I have a function abc() that will handle the logic related to analyzing the arguments passed to my script.
How can I pass all arguments my Bash script has received to abc()? The number of arguments is variable, so I can't just hard-code the arguments passed like this:
abc $1 $2 $3 $4
Better yet, is there any way for my function to have access to the script arguments' variables?
The $# variable expands to all command-line parameters separated by spaces. Here is an example.
abc "$#"
When using $#, you should (almost) always put it in double-quotes to avoid misparsing of arguments containing spaces or wildcards (see below). This works for multiple arguments. It is also portable to all POSIX-compliant shells.
It is also worth noting that $0 (generally the script's name or path) is not in $#.
The Bash Reference Manual Special Parameters Section says that $# expands to the positional parameters starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word. That is "$#" is equivalent to "$1" "$2" "$3"....
Passing some arguments:
If you want to pass all but the first arguments, you can first use shift to "consume" the first argument and then pass "$#" to pass the remaining arguments to another command. In Bash (and zsh and ksh, but not in plain POSIX shells like dash), you can do this without messing with the argument list using a variant of array slicing: "${#:3}" will get you the arguments starting with "$3". "${#:3:4}" will get you up to four arguments starting at "$3" (i.e. "$3" "$4" "$5" "$6"), if that many arguments were passed.
Things you probably don't want to do:
"$*" gives all of the arguments stuck together into a single string (separated by spaces, or whatever the first character of $IFS is). This looses the distinction between spaces within arguments and the spaces between arguments, so is generally a bad idea. Although it might be ok for printing the arguments, e.g. echo "$*", provided you don't care about preserving the space within/between distinction.
Assigning the arguments to a regular variable (as in args="$#") mashes all the arguments together like "$*" does. If you want to store the arguments in a variable, use an array with args=("$#") (the parentheses make it an array), and then reference them as e.g. "${args[0]}" etc. Note that in Bash and ksh, array indexes start at 0, so $1 will be in args[0], etc. zsh, on the other hand, starts array indexes at 1, so $1 will be in args[1]. And more basic shells like dash don't have arrays at all.
Leaving off the double-quotes, with either $# or $*, will try to split each argument up into separate words (based on whitespace or whatever's in $IFS), and also try to expand anything that looks like a filename wildcard into a list of matching filenames. This can have really weird effects, and should almost always be avoided. (Except in zsh, where this expansion doesn't take place by default.)
I needed a variation on this, which I expect will be useful to others:
function diffs() {
diff "${#:3}" <(sort "$1") <(sort "$2")
}
The "${#:3}" part means all the members of the array starting at 3. So this function implements a sorted diff by passing the first two arguments to diff through sort and then passing all other arguments to diff, so you can call it similarly to diff:
diffs file1 file2 [other diff args, e.g. -y]
Use the $# variable, which expands to all command-line parameters separated by spaces.
abc "$#"
Here's a simple script:
#!/bin/bash
args=("$#")
echo Number of arguments: $#
echo 1st argument: ${args[0]}
echo 2nd argument: ${args[1]}
$# is the number of arguments received by the script. I find easier to access them using an array: the args=("$#") line puts all the arguments in the args array. To access them use ${args[index]}.
It's worth mentioning that you can specify argument ranges with this syntax.
function example() {
echo "line1 ${#:1:1}"; #First argument
echo "line2 ${#:2:1}"; #Second argument
echo "line3 ${#:3}"; #Third argument onwards
}
I hadn't seen it mentioned.
abc "$#" is generally the correct answer.
But I was trying to pass a parameter through to an su command, and no amount of quoting could stop the error su: unrecognized option '--myoption'. What actually worked for me was passing all the arguments as a single string :
abc "$*"
My exact case (I'm sure someone else needs this) was in my .bashrc
# run all aws commands as Jenkins user
aws ()
{
sudo su jenkins -c "aws $*"
}
abc "$#"
$# represents all the parameters given to your bash script.

What is the difference between ${#:2} and ${*:2} in these cases?

Given the following code in bash:
filename=${1}
content1=${#:2}
content2="${#:2}"
content3=${*:2}
content4="${*:2}"
echo "${content1}" > "${filename}"
echo "${content2}" >> "${filename}"
echo "${content3}" >> "${filename}"
echo "${content4}" >> "${filename}"
What is the difference between the "contents" ? When I will see the difference? What is the better way to save the content that I get and why?
In an assignment, the right-hand side doesn't have to be quoted to prevent word splitting and globbing, unless it contains a blank.
These two are the same:
myvar=$var
myvar="$var"
but these are not:
myvar='has space'
myvar=has space
The last one tries to run a command space with the environment variable myvar set to value has.
This means that content1 is the same as content2, and content3 is the same as content4, as they only differ in RHS quoting.
The differences thus boil down to the difference between $# and $*; the fact that subarrays are used doesn't matter. Quoting from the manual:
(for $*):
When the expansion occurs within double quotes, it expands to a single word with the value of each parameter separated by the first character of the IFS special variable.
(for $#):
When the expansion occurs within double quotes, each parameter expands to a separate word.
Since you're using quotes (as you almost always should when using $# or $*), the difference is that "${*:2}" is a single string, separated by the first character of IFS, and "${#:2}" expands to separate words, blank separated.
Example:
$ set -- par1 par2 par3 # Set $1, $2, and $3
$ printf '<%s>\n' "${#:2}" # Separate words
<par2>
<par3>
$ printf '<%s>\n' "${*:2}" # Single word, blank separated (default IFS)
<par2 par3>
$ IFS=, # Change IFS
$ printf '<%s>\n' "${*:2}" # Single word, comma separated (new IFS)
<par2,par3>
As for when to use $# vs. $*: as a rule of thumb, you almost always want "$#" and almost never the unquoted version of either, as the latter is subject to word splitting and globbing.
"$*" is useful if you want to join array elements into a single string as in the example, but the most common case, I guess, is iterating over positional parameters in a script or function, and that's what "$#" is for. See also this Bash Pitfall.
The only difference is that using $* will cause the arguments to be concatenated with the first character of IFS, while $# will cause the arguments to be concatenated with a single space (regardless of the value of IFS):
$ set a b c
$ IFS=-
$ c1="$*"
$ c2="$#"
$ echo "$c1"
a-b-c
$ echo "$c2"
a b c
The quotes aren't particularly important here, as expansions on the right-hand side of an assignment aren't subject to word-splitting or pathname expansion; content1 and content2 should always be identical, as should be content3 and content4.
Which is better depends on what you want.

Bash last index of

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.

What does $* mean in bash scripting?

What does $* mean in bash scripting?
I tried to search on google for it, but I found only about $0, $1 and so on.
From the man page:
* Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, it expands to a single
word with the value of each parameter separated by the first character of the IFS special variable. That is, "$*" is equivalent
to "$1c$2c...", where c is the first character of the value of the IFS variable. If IFS is unset, the parameters are separated
by spaces. If IFS is null, the parameters are joined without intervening separators.
So it is equivalent to all the positional parameters, with slightly different semantics depending on whether or not it is in quotes.
See this page:
http://tldp.org/LDP/abs/html/internalvariables.html#IFSEMPTY
The behavior of $* and $# when $IFS is empty depends
+ on which Bash or sh version being run.
It is therefore inadvisable to depend on this "feature" in a script.
It's all the arguments passed to the script, except split by word. You almost always want to use "$#" instead. And it's all in the bash(1) man page.
Its the list of arguments supplied on the command line to the script .$0 will be the script name.
It's a space separated string of all arguments. For example, if $1 is "hello" and $2 is "world", then $* is "hello world". (Unless $IFS is set; then it's an $IFS separated string.)
You can use symbolhound search engine to find codes that google will not look for.
For your query click here
If you see $ in prefix with anything , it means its a variable. The value of the variable is used.
Example:
count=100
echo $count
echo "Count Value = $count"
Output of the above script:
100
Count Value = 100
As an independent command it doesn't have any significance in bash scripting.
But, as per usage in commands, it's used to indicate common operation on files / folders with some common traits.
and with grep used to represent zero or more common traits in a command.

Resources