Variable in for loop digits [duplicate] - linux

This question already has answers here:
How do I iterate over a range of numbers defined by variables in Bash?
(20 answers)
Closed 8 years ago.
How can I use variable in for loop digits?
for example:
num="12"
for i in {0..$num}; do
...
done

Brace expansion with variables doesn't work as one would expect (see Appendix B for juicy details), i.e. {0..$num} would only return {0..12} literally instead of a list of number.
Try seq instead like this:
num="12"
for i in $(seq 0 $num); do
echo $i
done
Appendix B: Juicy details
The bash manual saith,
The order of expansions is: brace expansion, tilde expansion, parameter, variable, and arithmetic expansion and command substitution (done in a left-to-right fashion), word splitting, and filename expansion.
At the time shell expands {0..$num} (brace expansion), $num isn't expanded (variable expansion) yet. The sequence expression a..b needs both a and b to be numbers to generate a sequence, but here we have one number and one non-number (the literal string $num). Failing this, the shell falls back to interpreting {0..$num} literally. Then variable expansion takes over, and finally we get {0..12}

Bash does brace expansion before variable expansion, so you will get output like {1..12}. With use of eval you can get it to work.
Test:
$ num=5
$ for i in {1..$num}; do echo "$i"; done
{1..5}
$ for i in $(eval echo {1..$num}); do echo "$i"; done
1
2
3
4
5
Please note: eval is evil in disguise.

Related

how to use variables with brace expansion [duplicate]

This question already has answers here:
Brace expansion with variable? [duplicate]
(6 answers)
Closed 4 years ago.
I have four files:
1.txt 2.txt 3.txt 4.txt
in linux shell, I could use :
ls {1..4}.txt to list all the four files
but if I set two variables : var1=1 and var2=4, how to list the four files?
that is:
var1=1
var2=4
ls {$var1..$var2}.txt # error
what is the correct code?
Using variables with the sequence-expression form ({<numFrom>..<numTo>}) of brace expansion only works in ksh and zsh, but, unfortunately, not in bash (and (mostly) strictly POSIX-features-only shells such as dash do not support brace expansion at all, so brace expansion should be avoided with /bin/sh altogether).
Given your symptoms, I assume you're using bash, where you can only use literals in sequence expressions (e.g., {1..3}); from the manual (emphasis mine):
Brace expansion is performed before any other expansions, and any characters special to other expansions are preserved in the result.
In other words: at the time a brace expression is evaluated, variable references have not been expanded (resolved) yet; interpreting literals such as $var1 and $var2 as numbers in the context of a sequence expression therefore fails, so the brace expression is considered invalid and as not expanded.
Note, however, that the variable references are expanded, namely at a later stage of overall expansion; in the case at hand the literal result is the single word '{1..4}' - an unexpanded brace expression with variable values expanded.
While the list form of brace expansion (e.g., {foo,bar)) is expanded the same way, later variable expansion is not an issue there, because no interpretation of the list elements is needed up front; e.g. {$var1,$var2} correctly results in the 2 words 1 and 4.
As for why variables cannot be used in sequence expressions: historically, the list form of brace expansion came first, and when the sequence-expression form was later introduced, the order of expansions was already fixed.
For a general overview of brace expansion, see this answer.
Workarounds
Note: The workarounds focus on numerical sequence expressions, as in the question; the eval-based workaround also demonstrates use of variables with the less common character sequence expressions, which produce ranges of English letters (e.g., {a..c} to produce a b c).
A seq-based workaround is possible, as demonstrated in Jameson's answer.
A small caveat is that seq is not a POSIX utility, but most modern Unix-like platforms have it.
To refine it a little, using seq's -f option to supply a printf-style format string, and demonstrating two-digit zero-padding:
seq -f '%02.f.txt' $var1 $var2 | xargs ls # '%02.f'==zero-pad to 2 digits, no decimal places
Note that to make it fully robust - in case the resulting words contain spaces or tabs - you'd need to employ embedded quoting:
seq -f '"%02.f a.txt"' $var1 $var2 | xargs ls
ls then sees 01 a.txt, 02 a.txt, ... with the argument boundaries correctly preserved.
If you want to robustly collect the resulting words in a Bash array first, e.g., ${words[#]}:
IFS=$'\n' read -d '' -ra words < <(seq -f '%02.f.txt' $var1 $var2)
ls "${words[#]}"
The following are pure Bash workarounds:
A limited workaround using Bash features only is to use eval:
var1=1 var2=4
# Safety check
(( 10#$var1 + 10#$var2 || 1 )) 2>/dev/null || { echo "Need decimal integers." >&2; exit 1; }
ls $(eval printf '%s\ ' "{$var1..$var2}.txt") # -> ls 1.txt 2.txt 3.txt 4.txt
You can apply a similar technique to a character sequence expression;
var1=a var2=c
# Safety check
[[ $var1 == [a-zA-Z] && $var2 == [a-zA-Z] ]] || { echo "Need single letters."; exit 1; }
ls $(eval printf '%s\ ' "{$var1..$var2}.txt") # -> ls a.txt b.txt c.txt
Note:
A check is performed up front to ensure that $var1 and $var2 contain decimal integers or single English letters, which then makes it safe to use eval. Generally, using eval with unchecked input is a security risk and use of eval is therefore best avoided.
Given that output from eval must be passed unquoted to ls here, so that the shell splits the output into individual arguments through words-splitting, this only works if the resulting filenames contain no embedded spaces or other shell metacharacters.
A more robust, but more cumbersome pure Bash workaround to use an array to create the equivalent words:
var1=1 var2=4
# Emulate brace sequence expression using an array.
args=()
for (( i = var1; i <= var2; i++ )); do
args+=( "$i.txt" )
done
ls "${args[#]}"
This approach bears no security risk and also works with resulting filenames with embedded shell metacharacters, such as spaces.
Custom increments can be implemented by replacing i++ with, e.g., i+=2 to step in increments of 2.
Implementing zero-padding would require use of printf; e.g., as follows:
args+=( "$(printf '%02d.txt' "$i")" ) # -> '01.txt', '02.txt', ...
For that particular piece of syntax (a "sequence expression") you're out of luck, see Bash man page:
A sequence expression takes the form {x..y[..incr]}, where x and y are
either integers or single characters, and incr, an optional increment,
is an integer.
However, you could instead use the seq utility, which would have a similar effect -- and the approach would allow for the use of variables:
var1=1
var2=4
for i in `seq $var1 $var2`; do
ls ${i}.txt
done
Or, if calling ls four times instead of once bothers you, and/or you want it all on one line, something like:
for i in `seq $var1 $var2`; do echo ${i}.txt; done | xargs ls
From seq(1) man page:
seq [OPTION]... LAST
seq [OPTION]... FIRST LAST
seq [OPTION]... FIRST INCREMENT LAST

How is Bash interpreting these statements? [duplicate]

This question already exists:
Assign and use of a variable in the same subshell [duplicate]
Closed 7 years ago.
$ a=one; echo $a
one
$ a=two echo $a
one
$ echo $a
one
The value of a will be initialized to 'two' but why is it displaying 'one'?
Same line initialization of env variables is only available for sub shells.
It will be more evident from these examples:
unset a
a=two echo "<$a>"
<>
a=two bash -c 'echo "<$a>"'
<two>
echo "<$a>"
<>
In the 2nd snippet it prints <two> because we're spawning a sub-shell using bash -c.
This is due to the order in which the shell processes substitutions and variables. According to POSIX rules for a shell:
When a given simple command is required to be executed (that is, when
any conditional construct such as an AND-OR list or a case statement
has not bypassed the simple command), the following expansions,
assignments, and redirections shall all be performed from the
beginning of the command text to the end:
The words that are recognized as variable assignments or redirections according to Shell Grammar Rules are saved for processing
in steps 3 and 4.
The words that are not variable assignments or redirections shall be expanded. If any fields remain following their expansion, the first
field shall be considered the command name and remaining fields are
the arguments for the command.
Redirections shall be performed as described in Redirection.
Each variable assignment shall be expanded for tilde expansion, parameter expansion, command substitution, arithmetic expansion, and
quote removal prior to assigning the value.
That means that the variable assignment is performed after parameter expansion has occurred, e.g. $a is expanded before the a= variable assignment for that command has been processed. The new value of a will be passed into the execution environment of the command but it won't be of any use in this case with echo.
You can perhaps see better what's going on if you use a variable that does not already exist.
$ unset a
$ a=two echo \($a\)
()
$ a=one
$ a=two echo \($a\)
(one)
Here's an example where you can see that the variable assignment is passed to the execution environment of the command that is executed:
$ a=one
$ a=two python -c 'import os;print(os.environ["a"])'
two

Linux Bash's rule for determing a variable name Greedy or Non-Greedy

If you run the following bash script on Ubuntu 12.04:
t="t"
echo "1st Output this $15"
echo "2nd this $test"
The output is:
1st Output this 5
2nd this
How can the first echo takes $1 as a variable name (non-greedy) interpreting it as ${1}5 while the second echo takes $test as a variable name (greedy) instead of interpreting it as ${t}est?
There are two parts to your question:
$15 would always be interpreted as $1, i.e. the first positional parameter1, concatenated with 5. In order to use the fifteenth positional parameter, you'd need to say ${15}.
$test would be interpreted as variable test. So if you want it to be interpreted as $t concatenated with est, then you need to say ${t}est
1Quoting from info bash:
When a positional parameter consisting of more than a single digit is
expanded, it must be enclosed in braces (see EXPANSION below).
...
EXPANSION
Expansion is performed on the command line after it has been split into
words. There are seven kinds of expansion performed: brace expansion,
tilde expansion, parameter and variable expansion, command substituā€
tion, arithmetic expansion, word splitting, and pathname expansion.
You may also want to refer to:
What's the difference between ${varname} and $varname in a shell scripts

expanding variables inside {..} in bash? [duplicate]

This question already has answers here:
Brace expansion with a Bash variable - {0..$foo}
(5 answers)
Closed 8 years ago.
I have the following shell script
cat test.sh
j=00000001;
k=00000005;
l=$(echo {00000001..00000005}.jpg);
m=$(echo {$j..$k}.jpg);
ls $l
ls $m
Here is the output
./test.sh
00000001.jpg 00000002.jpg 00000003.jpg 00000004.jpg 00000005.jpg
ls: cannot access {00000001..00000005}.jpg: No such file or directory
My doubt is "Why is the ls $m not working".
and How to make that work?
Thanks in advance.
lin
Sequence expansion only happens for literal numbers. Variable expansion occurs after sequence expansion:
A sequence expression takes the form {x..y}, where x and y are either
integers or single characters. When integers are supplied, the expression
expands to each number between x and y, inclusive. When characters are
supplied, the expression expands to each character lexicographically
between x and y, inclusive. Note that both x and y must be of the same type.
Brace expansion is performed before any other expansions, and any characters
special to other expansions are preserved in the result. It is strictly textual.
Bash does not apply any syntactic interpretation to the context of the expansion
or the text between the braces.
For your case, you can use eval:
m=`eval echo {$j..$k}.jpg`

How to echo "$x_$y" in Bash script?

It is very interesting that if you intend to display 0_1 with Bash using the code
x=0
y=1
echo "$x_$y"
then it will only display
1
I tried echo "$x\_$y" and it doesn't work.
How can I echo the form $x_$y? I'm going to use it on a file name string.
Because variable names are allowed to have underscores in them, the command:
echo "$x_$y"
is trying to echo ${x_} (which is probably empty in your case) followed by ${y}. The reason for this is because parameter expansion is a greedy operation - it will take as many legal characters as possible after the $ to form a variable name.
The relevant part of the bash manpage states:
The $ character introduces parameter expansion, command substitution, or arithmetic expansion.
The parameter name or symbol to be expanded may be enclosed in braces, which are optional but serve to protect the variable to be expanded from characters immediately following it which could be interpreted as part of the name.
When braces are used, the matching ending brace is the first } not escaped by a backslash or within a quoted string, and not within an embedded arithmetic expansion, command substitution, or parameter expansion.
Hence, the solution is to ensure that the _ is not treated as part of the first variable, which can be done with:
echo "${x}_${y}"
I tend to do all my bash variables like this, even standalone ones like:
echo "${x}"
since it's more explicit, and I've been bitten so many times in the past :-)
This way:
$ echo "${x}_${y}"
0_1
wrap it in curly braces:
echo "${x}_${y}"
Just to buck the trend, you can also do this:
echo $x'_'$y
You can have quoted and unquoted parts next to each other with no space between. And since ' isn't a legal character for a variable name, bash will substitute only $x. :)

Resources