Using $# in bash loops - bash

I am trying to understand why this loop does not print a number for each arguments supplied to the script.
#!/bin/bash
for i in {1..$#}; do
echo $i
done
Instead, when supplied e.g. 3 arguments, it outputs
{1..3}

The expression {} does not accept variables.
To do so, you need to work with for example seq. The following will make it::
#!/bin/bash
for i in $(seq 1 $#); do
echo $i
done
Note that $() is equivalent to ``. That is, it performs a command substitution. For example:
$ d=$(echo "hello")
$ echo $d
hello
You can see more information in Shell Programming: What's the difference between $(command) and command.
Tests
$ ./a
$
$ ./a a b c
1
2
3

Brace expansion occurs before variable expansion
http://www.gnu.org/software/bash/manual/bashref.html#Shell-Expansions

Related

Bash variable not working properly in a for loop [duplicate]

Here is myscript.sh
#!/bin/bash
for i in {1..$1};
do
echo $1 $i;
done
If I run myscript.sh 3 the output is
3 {1..3}
instead of
3 1
3 2
3 3
Clearly $3 contains the right value, so why doesn't for i in {1..$1} behave the same as if I had written for i in {1..3} directly?
You should use a C-style for loop to accomplish this:
for ((i=1; i<=$1; i++)); do
echo $i
done
This avoids external commands and nasty eval statements.
Because brace expansion occurs before expansion of variables. http://www.gnu.org/software/bash/manual/bashref.html#Brace-Expansion.
If you want to use braces, you could so something grim like this:
for i in `eval echo {1..$1}`;
do
echo $1 $i;
done
Summary: Bash is vile.
You can use seq command:
for i in `seq 1 $1`
Or you can use the C-style for...loop:
for((i=1;i<=$1;i++))
Here is a way to expand variables inside braces without eval:
end=3
declare -a 'range=({'"1..$end"'})'
We now have a nice array of numbers:
for i in ${range[#]};do echo $i;done
1
2
3
I know you've mentioned bash in the heading, but I would add that 'for i in {$1..$2}' works as intended in zsh. If your system has zsh installed, you can just change your shebang to zsh.
Using zsh with the example 'for i in {$1..$2}' also has the added benefit that $1 can be less than $2 and it still works, something that would require quite a bit of messing about if you wanted that kind of flexibility with a C-style for loop.

How to write for loop that will create specified numbered files [duplicate]

Here is myscript.sh
#!/bin/bash
for i in {1..$1};
do
echo $1 $i;
done
If I run myscript.sh 3 the output is
3 {1..3}
instead of
3 1
3 2
3 3
Clearly $3 contains the right value, so why doesn't for i in {1..$1} behave the same as if I had written for i in {1..3} directly?
You should use a C-style for loop to accomplish this:
for ((i=1; i<=$1; i++)); do
echo $i
done
This avoids external commands and nasty eval statements.
Because brace expansion occurs before expansion of variables. http://www.gnu.org/software/bash/manual/bashref.html#Brace-Expansion.
If you want to use braces, you could so something grim like this:
for i in `eval echo {1..$1}`;
do
echo $1 $i;
done
Summary: Bash is vile.
You can use seq command:
for i in `seq 1 $1`
Or you can use the C-style for...loop:
for((i=1;i<=$1;i++))
Here is a way to expand variables inside braces without eval:
end=3
declare -a 'range=({'"1..$end"'})'
We now have a nice array of numbers:
for i in ${range[#]};do echo $i;done
1
2
3
I know you've mentioned bash in the heading, but I would add that 'for i in {$1..$2}' works as intended in zsh. If your system has zsh installed, you can just change your shebang to zsh.
Using zsh with the example 'for i in {$1..$2}' also has the added benefit that $1 can be less than $2 and it still works, something that would require quite a bit of messing about if you wanted that kind of flexibility with a C-style for loop.

Why is bash swallowing -e in the front of an array [duplicate]

This question already has answers here:
How do I echo "-e"?
(6 answers)
Closed 9 years ago.
Given the following syntax:
x=(-a 2);echo "${x[#]}";x=(-e 2 -e); echo "${x[#]}"
Output:
-a 2
2 -e
Desired output
-a 2
-e 2 -e
Why is this happening? How do I fix?
tl;dr
printf "%s\n" "${x[*]}"
Explanation
echo takes 3 options:
$ help echo
[…]
Options:
-n do not append a newline
-e enable interpretation of the following backslash escapes
-E explicitly suppress interpretation of backslash escapes
So if you run:
$ echo -n
$ echo -n -e
$ echo -n -e -E
You get nothing. Even if you put each option in quotes, it still looks the same to bash:
$ echo "-n"
$ echo "-n" "-e"
The last command runs echo with two arguments: -n and -e. Now contrast that with:
$ echo "-n -e"
-n -e
What we did was run echo with a single argument: -n -e. Since bash does not recognize the (combined) option -n -e, it finally echoes the single argument to the terminal like we want.
Applied to Arrays
In the second case, the array x begins with the element -e. After bash expands the array ${x[#]}, you are effectively running:
$ echo "-e" "2" "-e"
2 -e
Since the first argument is -e, it is interpreted as an option (instead of echoed to the terminal), as we already saw.
Now contrast that with the other style of array expansion ${x[*]}, which effectively does the following:
$ echo "-e 2 -e"
-e 2 -e
bash sees the single argument -e 2 -e — and since it does not recognize that as an option — it echoes the argument to the terminal.
Note that ${x[*]} style expansion is not safe in general. Take the following example:
$ x=(-e)
$ echo "${x[*]}"
Nothing is printed even though we expected -e to be echoed. If you've been paying attention, you already know why this is the case.
Escaping
The solution is to escape any arguments to the echo command. Unfortunately, unlike other commands which offer some way to say, “hey! the following argument is not to be interpreted as an option” (typically a -- argument), bash provides no such escaping mechanism for echo.
Fortunately there is the printf command, which provides a superset of the functionality that echo offers. Hence we arrive at the solution:
printf "%s\n" "${x[*]}"
#MichaelKropat's answer gives sufficient explanation.
As an alternative to echo (and printf), cat and a bash here-string can be used:
$ x=(-a 2);cat <<< "${x[#]}";x=(-e 2 -e); cat <<< "${x[#]}"
-a 2
-e 2 -e
$
Nice one!
What is happening is the first -e is being interpreted as an option for echo (to enable escape sequences'
Usually, you'd do something like echo -- "-e", and it should print simply -e, but echo is happy to behave differently, and simply prints out -- -e as a whole string.
echo does not interpret -- to mean the end of options.
The solution to the problem could also be found in the man pages:
Due to shell aliases and built-in echo command, using an unadorned
echo interactively or in a script may get you different functionality
than that described here. Invoke it via env (i.e., env echo ...)
to avoid interference from the shell.
So something like this should work:
x=(-a 2);echo "${x[#]}";x=(-e 2 -e); env echo "${x[#]}"

Using a variable in brace expansion range fed to a for loop

Here is myscript.sh
#!/bin/bash
for i in {1..$1};
do
echo $1 $i;
done
If I run myscript.sh 3 the output is
3 {1..3}
instead of
3 1
3 2
3 3
Clearly $3 contains the right value, so why doesn't for i in {1..$1} behave the same as if I had written for i in {1..3} directly?
You should use a C-style for loop to accomplish this:
for ((i=1; i<=$1; i++)); do
echo $i
done
This avoids external commands and nasty eval statements.
Because brace expansion occurs before expansion of variables. http://www.gnu.org/software/bash/manual/bashref.html#Brace-Expansion.
If you want to use braces, you could so something grim like this:
for i in `eval echo {1..$1}`;
do
echo $1 $i;
done
Summary: Bash is vile.
You can use seq command:
for i in `seq 1 $1`
Or you can use the C-style for...loop:
for((i=1;i<=$1;i++))
Here is a way to expand variables inside braces without eval:
end=3
declare -a 'range=({'"1..$end"'})'
We now have a nice array of numbers:
for i in ${range[#]};do echo $i;done
1
2
3
I know you've mentioned bash in the heading, but I would add that 'for i in {$1..$2}' works as intended in zsh. If your system has zsh installed, you can just change your shebang to zsh.
Using zsh with the example 'for i in {$1..$2}' also has the added benefit that $1 can be less than $2 and it still works, something that would require quite a bit of messing about if you wanted that kind of flexibility with a C-style for loop.

How to wrap another shell still passing $OPTIND as-is?

I'm trying to wrap a bash script b with a script a.
However I want to pass the options passed to a also to b as they are.
#!/bin/bash
# script a
./b ${#:$OPTIND}
This will also print $1 (if any). What's the simplest way not to?
So calling:
./a -c -d 5 first-arg
I want b to execute:
./b -c -d 5 # WITHOUT first-arg
In bash, you can build an array containing the options, and use that array to call the auxiliary program.
call_b () {
typeset -i i=0
typeset -a a; a=()
while ((++i <= OPTIND)); do # for i=1..$OPTIND
a+=("${!i}") # append parameter $i to $a
done
./b "${a[#]}"
}
call_b "$#"
In any POSIX shell (ash, bash, ksh, zsh under sh or ksh emulation, …), you can build a list with "$1" "$2" … and use eval to set different positional parameters.
call_b () {
i=1
while [ $i -le $OPTIND ]; do
a="$a \"\$$i\""
i=$(($i+1))
done
eval set -- $a
./b "$#"
}
call_b "$#"
As often, this is rather easier in zsh.
./b "${(#)#[1,$OPTIND]}"
Why are you using ${#:$OPTIND} and not just $# or $*?
The ${parameter:index} syntax says to use index to parse $parameter. If you're using $#, it'll use index as an index into the parameters.
$ set one two three four #Sets "$#"
$ echo $#
one two three four
$ echo ${#:0}
one two three four
$ echo ${#:1}
one two three four
$ echo ${#:2}
two three four
$OPTIND is really only used if you're using getopts. This counts the number of times getopts processes the parameters in $#. According to the bash manpage:
OPTIND is initialized to 1 each time the shell or a shell script is invoked.
Which may explain why you're constantly getting the value of 1.
EDITED IN RESPONSE TO EDITED QUESTION
#David - "./b $# " still prints the arguments of passed to a (see Q edit). I want to pass only the options of a and not the args
So, if I executed:
$ a -a foo -b bar -c fubar barfu barbar
You want to pass to b:
$ b -a foo -b bar -c fubar
but not
$ b -arg1 foo -arg2 bar -arg3 fubar barfu barbar
That's going to be tricky...
Is there a reason why you can't pass the whole line to b and just ignore it?
I believe it might be possible to use regular expressions:
$ echo "-a bar -b foo -c barfoo foobar" | sed 's/\(-[a-z] [^- ][^- ]*\) *\([^-][^-]*\)$/\1/'
-a bar -b foo -c barfoo
I can't vouch that this regular expression will work in all situations (i.e. what if there are no parameters?). Basically, I'm anchoring it to the end of the line, and then matching for the last parameter and argument and the rest of the line. I do a replace with just the last parameter and argument.
I've tested it in a few situations, but you might simply be better off using getopts to capture the arguments and then passing those to b yourself, or simply have b ignore those extra arguments if possible.
In order to separate the command options from the regular arguments, you need to know which options take arguments, and which stand alone.
In the example command:
./a -c -d 5 first-arg
-c and -d might be standalone options and 5 first-arg the regular arguments
5 might be an argument to the -d option (this seems to be what you mean)
-d might be an argument to the -c option and (as in the first case) 5 first-arg the regular arguments.
Here's how I'd handle it, assuming -a, -b, -c and -d are the only options, and that -b and -d is the only ones that take an option argument. Note that it is necessary to parse all of the options in order to figure out where they end.
#!/bin/bash
while getopts ab:cd: OPT; do
case "$OPT" in
a|b|c|d) : ;; # Don't do anything, we're just here for the parsing
?) echo "Usage: $0 [-ac] [-b something] [-d something] [args...]" >&2
exit 1 ;;
esac
done
./b "${#:1:$((OPTIND-1))}"
The entire while loop is there just to compute OPTIND. The ab:cd: in the getopts command defines what options are allowed and which ones take arguments (indicated by colons). The cryptic final expression means "elements 1 through OPTIND-1 of the argument array, passed as separate words".

Resources