what did I do wrong in initializing my array with 0 ? - linux

when I check the length of the array is always 1 even I give more parameters in the command line
for i in $*
do
echo $i
conect[$i]=0
done
echo ${#conect}

Try this:
#!/bin/bash
declare -A conect
for i in "$#"
do
echo $i
conect[$i]=0
done
echo ${#conect[#]}
Explanation:
An associative array (i.e. indexes can be non-numeric) must be declared with declare -A. You do not need this if indexes are guaranteed to be numeric.
${#foo} is the length (number of characters) of a string-valued variable; ${#conect[#]} is the length (number of elements) of an array.
As pointed out by others, "$#" is better than $*, especially when (quoted) parameters may contain spaces.

You should use an array:
for i in "$#"

$* create one single argument separated with IFS. that's why. Use $#
What is the difference between "$#" and "$*" in Bash?
Edit
Actually, as pointed out by #that_other_guy and #Ruud_Helderman (thanks to you both), what I said isn't quite right.
First thing is the Mea Culpa, as this matters isn't the full solution.
But it made me wonders so here is the correct way.
The IFS difference is a fact. But this solely matters if you quote "$*" or "$#"
for i in "$*"
do
echo $i
done
Will output every arguments on the same line whereas
for i in "$#"
do
echo $i
done
Will do it one at a time.

Related

Strange behavior with parameter expansion in program arguments

I'm trying to conditionally pass an argument to a bash script only if it has been set in the calling script and I've noticed some odd behavior.
I'm using parameter expansion to facilitate this, outputting an option only if the corresponding variable is set. The aim is to pass an argument from a 'parent' script to a 'child' script.
Consider the following example:
The calling script:
#!/bin/bash
# 1.sh
ONE="TEST_ONE"
TWO="TEST_TWO"
./2.sh \
--one "${ONE}" \
"${TWO:+"--two ${TWO}"}" \
--other
and the called script:
#!/bin/bash
# 2.sh
while [[ $# -gt 0 ]]; do
key="${1}"
case $key in
-o|--one)
ONE="${2}"
echo "ONE: ${ONE}"
shift
shift
;;
-t|--two)
TWO="${2}"
echo "TWO: ${TWO}"
shift
shift
;;
-f|--other)
OTHER=1
echo "OTHER: ${OTHER}"
shift
;;
*)
echo "UNRECOGNISED: ${1}"
shift
;;
esac
done
output:
ONE: TEST_ONE
UNRECOGNISED: --two TEST_TWO
OTHER: 1
Observe the behavior of the option '--two', which will be unrecognised. It looks like it is being expanded correctly, but is not recognised as being two distinct strings.
Can anyone explain why this is happening? I've seen it written in one source that it will not work with positional parameter arguments, but I'm still not understanding why this behaves as it does.
It is because when you pass $2 as a result of parameter expansion from 1.sh you are quoting it in a way that --two TEST_TWO is evaluated as one single argument, so that the number of arguments in 2.sh result in 4 instead of 5
But that said, using your $2 as ${TWO:+--two ${TWO}} would solve the problem, but that would word-split the content of $2 if it contains spaces. You need to use arrays.
As a much more recommended and fail-proof approach use arrays as below on 1.sh as
argsList=(--one "${ONE}" ${TWO:+--two "${TWO}"} --other)
and pass it along as
./2.sh "${argsList[#]}"
or if you are familiar with how quoting rules work (how and when to quote to prevent word-splitting from happening) use it directly on the command line as below. This would ensure that the contents variables ONE and TWO are preserved even if they have spaces.
./2.sh \
--one "${ONE}" \
${TWO:+--two "${TWO}"} \
--other
As a few recommended guidelines
Always use lower-case variable names for user defined variables to not confuse them with the environment variables maintained by the shell itself.
Use getopts() for more robust argument flags parsing

error using WHILE condition bash [duplicate]

This question already has answers here:
Print bash arguments in reverse order
(5 answers)
Closed 6 years ago.
I was trying to write a script that print the arguments in reverse order.
So if I type bash reverse.sh one two three
I expect my output to be three two one
How can i do this?
This is what I tried and it obviously didn't work...
#!/bin/bash
i=0
a="$"
for word in $*; do
echo $a$(($#-i))
i=$((i+1))
done
This is the output i get
$3
$2
$1
I thought this would print the parameters in order 3, 2, 1 but it didn't. How should I do it? Any help will be much appreciated. Thank you.
Let's define your arguments:
$ set -- one two three
Now, let's print them out in reverse order:
$ for ((i=$#;i>=1;i--)); do echo "${!i}"; done
three
two
one
How it works
for ((i=$#;i>=1;i--)) starts a loop in which i counts down from $# to 1. For each value of i, we print the corresponding positional parameter by ${!i}. The construct ${!i} uses indirection: instead of returning the value of i, ${!i} returns the value of the variable whose name is $i.
As a script
In a multi-line script form, we can use:
$ cat reverse
#!/bin/bash
for ((i=$#;i>=1;i--))
do
echo "${!i}"
done
As an example:
$ bash reverse One Two Three
Three
Two
One
Alternative: using tac
Another way to print things in reverse order is to use the utility tac. Consider this script:
$ cat reverse2
#!/bin/bash
printf "%s\n" "$#" | tac
Here is an example:
$ bash reverse2 Uno Dos Tres
Tres
Dos
Uno
printf "%s\n" "$#" prints out the positional parameters one per line. tac prints those lines in reverse order.
Limitation: The tac method only works correctly if the arguments do not themselves contain newlines.
You need eval with echo i.e. you need to evaluate the expansion, not output it:
eval echo $a$(($#-i))
Note that, using eval in general is discouraged as this could result in security implications if the input string is not sanitized. Check John1024's answer to see how this can be done without eval.

Shell Script working with multiple files [duplicate]

This question already has answers here:
How to iterate over arguments in a Bash script
(9 answers)
Closed 5 years ago.
I have this code below:
#!/bin/bash
filename=$1
file_extension=$( echo $1 | cut -d. -f2 )
directory=${filename%.*}
if [[ -z $filename ]]; then
echo "You forgot to include the file name, like this:"
echo "./convert-pdf.sh my_document.pdf"
else
if [[ $file_extension = 'pdf' ]]; then
[[ ! -d $directory ]] && mkdir $directory
convert $filename -density 300 $directory/page_%04d.jpg
else
echo "ERROR! You must use ONLY PDF files!"
fi
fi
And it is working perfectly well!
I would like to create a script which I can do something like this: ./script.sh *.pdf
How can I do it? Using asterisk.
Thank you for your time!
Firstly realize that the shell will expand *.pdf to a list of arguments. This means that your shell script will never ever see the *. Instead it will get a list of arguments.
You can use a construction like the following:
#!/bin/bash
function convert() {
local filename=$1
# do your thing here
}
if (( $# < 1 )); then
# give your error message about missing arguments
fi
while (( $# > 0 )); do
convert "$1"
shift
done
What this does is first wrap your functionality in a function called convert. Then for the main code it first checks the number of arguments passed to the script, if this is less than 1 (i.e. none) you give the error that a filename should be passed. Then you go into a while loop which is executed as long as there are arguments remaining. The first argument you pass to the convert function which does what your script already does. Then the shift operation is performed, what this does is it throws away the first argument and then shifts all the remaining arguments "left" by one place, that is what was $2 now is $1, what was $3 now is $2, etc. By doing this in the while loop until the argument list is empty you go through all the arguments.
By the way, your initial assignments have a few issues:
you can't assume that the filename has an extension, your code could match a dot in some directory path instead.
your directory assignment seems to be splitting on . instead of /
your directory assignment will contain the filename if no absolute or relative path was given, i.e. only a bare filename
...
I think you should spend a bit more time on robustness
Wrap your code in a loop. That is, instead of:
filename=$1
: code goes here
use:
for filename in "$#"; do
: put your code here
done

When should I use "" to quote a value in shell test and in echo?

I'm writing bash script like this:
VF_ETH=$(command)
if [ -n "$VF_ETH" ] ; then
echo "ixgbevf eth: "$VF_ETH
fi
command is a linux command, my question is:
$VF_ETH is to get value of VF_ETH, why use "" to quote it in line2 in shell test?
if I do not use "" to quote it, will test failed?
if use "" to quote a value is to make it into string, why not use in echo in line3?
Thank you
Assuming you get an actual command stored in VF_ETH variable, which contains spaces. Now if you use if [ -n $VF_ETH ] and when shell expands the variable, there will be multiple parameters to -n whereas it expects only one. Hence you might get something like binary operator expected error.
Also in the echo command, it is not mandatory to have only one parameter. Hence even if you are not using double quotes, it works.
Hence to avoid it, always use double quotes while expanding variables.
Also use https://www.shellcheck.net/ to check your script and it will give you correct information on where your script is wrong/not as per standard.
You should always double quote variables used in command line arguments and within [ ... ] tests, for example:
ls "$var"
echo "$var"
[ -f "$var" ]
test -f "$var"
In all the above examples the commands used will receive different values with and without the double quotes, when the value of $var starts with a bunch of spaces and contains some spaces. For example:
var=" an apple"
echo "$var" # prints " an apple"
echo $var # prints "an apple", without the leading space
You don't need to quote them in simple assignments, for example:
a=$var
a=$var$b
If the assignment contains spaces or other special characters, then you have to quote, or escape those special characters:
a="$var $b"
a=$var\ $b
a=$var" "$b
All the above are equivalent, but the first one is probably the easiest to read, and therefore recommended.
You don't need to quote special variables that never have unsafe values, for example:
test $? = 0
If you're unsure, or not yet confident, then a good role of thumb is to double quote always.
For 1. and 2. If you set $VF_ETH="x -a -z x" and test it with code:
if [ -n $VF_ETH ] ; then
echo yes
else
echo nope
fi
the output will be nope as the the inside of the square brackets would expand to -n x AND -z x (nonempty and empty). Adding quotes around the variable would fix that.
Set $VF_ETH="*". Now if you echo $foo bash would do wildcard expansion and echo would output the contents of your current directory.

Proper Quoting in Bash: Attach numbers stored in i to a

I wanted to write a short shell script, which removes specified pages from a pdf. Maybe I'm doing that in a bit convoluted manner, but that is what I came up with so far:
#!/bin/bash
#This is a script to remove a specified page from a specified pdf.
set verbose
s="A1-$(($2-1))"
if [ n -ge 3 ]; then
for i in 2..$#
do
s+=A$(($($i)+1))-$(($($(($i+1)))-1))
done
fi
pdftk A="$1" cat $s A$(($($#)+1))-end output output.pdf
I know it's quite convoluted code and if you know about the working of pdftk, I would appreciate a hint to make it easier, but for now I just need to know how to substitute a variable into a variable name. E.g. if
i=2
a2=3
echo $a($i)
gave me 3, that would be great, but it doesn't. How do I achieve this?
bash allows indirect parameter expansion:
$ i=2
$ a2=3
$ var="a$i" # a2
$ echo "${!var}"
3
What you really seem to want, though, is an array:
$ a=([2]=3) # Or simply a[2]=3
$ i=2
$ echo "${a[i]}"
3
(This is really a stop-gap answer, as there is almost certainly a much simpler answer to your question that doesn't involve this type of indirect parameter manipulation.)
I think this much simpler script that will do what you want:
#!/bin/bash
inputfile=$1
shift
ranges=() from=1
for pageToOmit in "$#"; do
ranges+=( "A$from-$(( pageToOmit - 1))" )
from=$(( pageToOmit + 1 ))
done
ranges+=( "$from-end" )
pdftk A="$inputfile" cat "${ranges[#]}" output output.pdf
Using eval:
i=2
a2=3
eval echo \$a$i
eval b=\$a$i
echo $b

Resources