while loops offset in shell script - shell

I/P file has data as follows:
Y
REQUIRES Z
A
REQUIRES B
C
REQUIRES D
REQUIRES E
REQUIRES F
G
REQUIRES H
I
REQUIRES J
EXACT OUTPUT FILE REQUIRED:
Y REQUIRES Z
A REQUIRES B
C REQUIRES D
C REQUIRES E
C REQUIRES F
G REQUIRES H
I REQUIRES J
I am using while loops to traverse the file.
while read line
do
if (condition)
{..
}
while read anoterline
do
done
done <inputfile
The problem I am facing is that
when inner while loop traverses say 4 lines and i break the inner
loop the outer while loop's offset is set to the offset at which the
inner while has stopped.
So I am missing the 4 lined data in execution of my outer loop.
I need the outer while loop to start off from the offset at which it had stopped
.

There's no reason to use nested loops here.
item=
while read -r first second _; do
if [[ $first = REQUIRES ]]; then
printf '%s REQUIRES %s\n' "$item" "$second"
else
item=$first
fi
done <inputfile
Invocation and output demonstrated at http://ideone.com/JejDyG

You can do this using awk
$ awk 'NF==1{a=$1};NF>1{print a,$0}' file
Y REQUIRES Z
A REQUIRES B
C REQUIRES D
C REQUIRES E
C REQUIRES F
G REQUIRES H
I REQUIRES J

Something like
#!/bin/bash
while read -r line; do
[[ $line =~ ^[A-Z]$ ]] && one="$line"
[[ $line =~ ^REQUIRES(.*) ]] && echo "$one $line"
done < "file"
Example
> ./abovescript
Y REQUIRES Z
A REQUIRES B
C REQUIRES D
C REQUIRES E
C REQUIRES F
G REQUIRES H
I REQUIRES J

Related

Accessing variable value in BASH for loop

This question already has answers here:
Dynamic variable names in Bash
(19 answers)
Closed 1 hour ago.
I want to assign and print variable values within a for loop in BASH.
My code looks like this:
tea=(A B C D E F G)
c=0
for (( i=1; i<${#tea[#]}; i++ ));
do
eval "var$c=${tea[$i]}";
c=$((c+1));
echo "$var$c" >> example.txt
done
The output I get in my txt file is: 1 2 3 4 5 6. The output I expect is B C D E F G. I don't understand why am I getting this output, am I not assigning values to var$c correctly or this echo command cannot read my variable value? I would appreciate your help a lot.
One approach using a nameref:
tea=(A B C D E F G)
c=0
for (( i=1; i<${#tea[#]}; i++ ));
do
declare -n varC="var$c" # nameref
varC="${tea[$i]}"
c=$((c+1))
echo "$varC" >> example.txt
done
This generates:
$ cat example.txt
B
C
D
E
F
G

How to pass items to bash for loop on multiple lines?

I can do this in bash:
for n in a b c d e ; do
echo $n
done
If a, b, c, d, e turn out to be long lines, without using a separate variable, how do I put them each on a separate line in the for loop syntax?
Split the line with \:
$ for i in a \
> b \
> c \
> d ; do echo $i ; done
a
b
c
d
I'd use a "here document":
while read n; do
echo $n
done <<EOF
some detailed stuff here
other things on the next line, blah blah blah
EOF
Of course in this particular example you can replace the entire while loop with cat but I suppose your real code is more involved.
you can put a b c d e in a file and run a loop over that file.
while read line
do
echo "$line \n"
done < file

Bash: for in loop with brace expansion in list

I would like to write a for in loop in which the possible values are taken from another variable. The part that I can't figure out is how to include values that need brace expansion. For example:
TEXT="a b c d{a..c} e f"
for VAR in $TEXT; do echo $VAR; done
What i get is
a
b
c
d{a..c}
e
f
But what I want is
a
b
c
da
db
dc
e
f
Does anyone know how I can get this to work?
Thank you very much!
I fear that you may have to resort to using eval in this case...However, I'd suggest that you avoided using a loop:
eval "printf '%s\n' $TEXT"
Using eval means that the braces will be expanded, producing your desired outcome.
You can see what happens using set -x:
$ set -x
$ eval "printf '%s\n' $TEXT"
+ eval 'printf '\''%s\n'\'' a b c d{a..c} e f'
++ printf '%s\n' a b c da db dc e f
a
b
c
da
db
dc
e
f
Note that the expansion occurs before the list is passed as arguments to printf.
If you get to define $TEXT yourself and want to loop through each of its expanded elements, I'd suggest using an array instead:
text=( a b c d{a..c} e f )
for var in "${text[#]}"; do
# whatever with "$var"
done
This is the preferred solution as it doesn't involve using eval.
EDIT
While there's a time & place for eval, it's not the safest approach for the general case. See Why should eval be avoided in Bash, and what should I use instead? for some reasons why.
Use eval:
TEXT="a b c d{a..c} e f"
for VAR in $TEXT; do eval echo $VAR; done
Output:
a
b
c
da db dc
e
f
If you want to loop over every element, you'll have to nest your loops:
for VAR in $TEXT; do
for VAR2 in $(eval echo $VAR); do
echo $VAR2
done
done
Output:
a
b
c
da
db
dc
e
f
Or use the eval on TEXT rather than each element:
TEXT="$(eval echo "a b c d{a..c} e f")"
for VAR in $TEXT; do echo $VAR; done
Output:
a
b
c
da
db
dc
e
f
Without eval, but with command substitution:
$ text="$(echo a b c d{a..c} e f)"
$ echo "$text"
a b c da db dc e f
And, as mentioned in Tom's answer, an array is probably the better way to go here.

How to preserve spaces in Bash "for loop" expression

I have function that print lines with spaces and need to loop through the line with a for..in loop. How do I preserve the spaces and not tokenize based on them.
function getTwoThings
{
echo "A B C"
echo "X Y Z"
}
for L in `getTwoThings`
do
echo $L
done
for L in "`getTwoThings`"
do
echo $L
done
Produces either six things or one thing.
A
B
C
X
Y
Z
A B C X Y Z
How do I get it to produce two things?
Read it like this using process substitution:
while IFS= read -r line; do
echo "$line"
done < <(getTwoThings)
Output:
A B C
X Y Z

Terminal script to group files into folders

I have a directory of files. I want to group them together in batches into n directories.
So...
"MyFolder" has files A, B, C, D, E, F, G, H, I, J
I want to put them in to, say, 3 batches... batched like this:
Folder 1 - A, B, C, D
Folder 2 - E, F, G
Folder 3 - H, I, J
(and not like this... which is in only way I can work it out myself)
Folder 1 - A, D, G, J
Folder 2 - B, E, H
Folder 3 - C, F, I
Can anyone please advise me on how this can be done?
Thanks!
Here is a solution that uses the argument list (the only list available to POSIX shell without using advanced features from e.g. bash). Note the caveat; a sufficiently large number of files will not fit into the argument buffer. Also note that Stack Overflow incorrectly assumes $# (the length of the argument list) begins a comment, so the coloring is a bit off.
#!/bin/sh
num_folders=3 # default to 3 directories
if [ $# -gt 0 ]; then
num_folders=$1
shift
fi
# put all files into "argument" list
# (CAVEAT: this won't work on a dir with a LOT of files)
set - *
batch=$(( $# / $num_folders )) # files per folder
remainder=$(( $# % $num_folders )) # folders to get an "extra" file
i=1
while [ $i -le $num_folders ]; do
mkdir "Folder $i"
if [ $i -le $remainder ]
then j=0
else j=1
fi
while [ $j -le $batch ]; do
mv "$1" "Folder $i"
shift
j=$(( $j + 1 ))
done
i=$(( $i + 1 ))
done
Here is this script in action (I'm using # as a prompt to trigger a different color for commands vs output):
# touch A B C D E F G H I J
# ls *
A B C D E F G H I J
# sh ../batch-n.sh 3
# ls *
Folder 1:
A B C D
Folder 2:
E F G
Folder 3:
H I J
Here's what happens when n=4
# touch A B C D E F G H I J
# ls *
A B C D E F G H I J
# sh ../batch-n.sh 4
# ls *
Folder 1:
A B C
Folder 2:
D E F
Folder 3:
G H
Folder 4:
I J
I put this code in the parent directory (I therefore invoked it as ../batch-n.sh) because it would otherwise file itself. This will work just fine with files that have spaces in it, so long as they all fit into a single command (if ls * can't run, neither can this script).
Setup:
mkdir /tmp/so
cd /tmp/so
touch {A..Z} # create 26 files
Simplest thing that would work:
Live On Coliru
for a in Folder\ {1..8}; do mkdir -pv "$a"; read z y x w; mv -v "$z" "$y" "$x" "$w" "$a/"; done < <(ls ? | xargs -n4)
Or, prettier:
for a in Folder\ {1..8};
do mkdir -pv "$a"
read z y x w
mv -v "$z" "$y" "$x" "$w" "$a/"
done < <(ls ? | xargs -n4)
Alternatively, (slightly better with names containing whitespace)
for a in Folder\ {1..8}; do mkdir -pv "$a"; read z && read y && read x && read w; mv -v "$z" "$y" "$x" "$w" "$a/"; done < <(ls ?)

Resources