Bash Loop Help (want to echo aa, bb, cc) - bash

Loop logic always confuses me, this is probably a simple solution. My current loop:
for i in a b; do for j in a b; do echo $i$j; done; done
This loop prints the following output:
aa
ab
ba
bb
I would like for it to only print:
aa
bb
I just want it to match up the first two letters, then the second two letters and so on. Eventually I want to expand this over files in two different directories. So I want to print the first file name in dir1, then the first in dir2. Then the 2nd in dir1 and the 2nd in dir2. Just trying to simplify that and understand the logic first.

I would suggest using arrays to solve your problem:
dir1_files=( dir1/* )
dir2_files=( dir2/* )
for (( i = 0; i < ${#dir1_files[#]}; ++i )); do
echo "${dir1_files[i]} ${dir2_files[i]}"
done
This assumes that the number of files in each directory is the same.

Just check that they're the same?
for i in a b; do
for j in a b; do
if [[ "$i" = "$j" ]]; then
echo $i$j
fi
done
done
Or:
for i in a b; do for j in a b; do [[ "$i" = "$j" ]] && echo $i$j; done; done

Related

Loop over first 10 of 100 subdirectories

I have 100 subdirectories and I wanted to loop through the first ten of them in a bash for loop such like that:
for d in ./output/*[0..9];
do
echo $d
done
But the output seems not what I expected:
./output/405050
./output/405140
./output/405309
./output/405310
./output/405319
./output/500550
./output/500589
./output/500610
Why only 8 were printed and my question is how to select a fix number elements from this type of for loop.
*[0..9] loops over ones that end in a 0, 9, or .. If you had written *{0..9} that would loop over ones ending in a digit 0 through 9--closer, but still not right.
Try this loop, which reads the first 10 directory names in a loop. It's kinda obtuse. The primary idea is using while read ... < <(cmd) to read a command's output one line at a time. IFS= and -r are pedantic bits to handle directory names with whitespace and backslashes correctly.
while IFS= read -r dir; do
echo "$dir"
done < <(ls output/*/ | head -10)
Or use this more straightforward version with a counter:
i=0
for dir in output/*/; do
echo "$dir"
((++i < 10)) || break
done
Or this one storing the directories in an array:
dirs=(output/*/)
for dir in "${dirs[#]::10}"; do
echo "$dir"
done
You can make a counter:
#!/bin/bash
i=0;
for d in ./output/*/;
do
echo $d
echo ""
if [[ i == 10 ]]; then
break
fi
i+=1
done
With this you asure to get 10 folders.
I very important to do the last backslash to match only directories.

bash read strings and output as one key and multiple values

Assuming there is an input:
1,2,C
We are trying to output it as
KEY=1, VAL1=2, VAL2=C
So far trying to modify from here:
Is there a way to create key-value pairs in Bash script?
for i in 1,2,C ; do KEY=${i%,*,*}; VAL1=${i#*,}; VAL2=${i#*,*,}; echo $KEY" XX "$VAL1 XX "$VAL2"; done
Output:
1 XX 2,c XX c
Not entirely sure what the pound ("#") and % here mean above, making the modification kinda hard.
Could any guru enlighten? Thanks.
I would generally prefer easier to read code, as bash can get ugly pretty fast.
Try this:
key_values.sh
#!/bin/bash
IFS=,
count=0
# $* is the expansion of all the params passed in, i.e. $1, $2, $3, ...
for i in $*; do
# '-eq' is checking for equality, i.e. is $count equal to zero.
if [ $count -eq 0 ]; then
echo -n "KEY=$i"
else
echo -n ", VAL${count}=$i"
fi
count=$(( $count + 1 ))
done
echo
Example
key_values.sh 1,2,ABC,123,DEF
Output
KEY=1, VAL1=2, VAL2=ABC, VAL3=123, VAL4=DEF
Expanding on anishsane's comment:
$ echo $1
1,2,3,4,5
$ IFS=, read -ra args <<<"$1" # read into an array
$ out="KEY=${args[0]}"
$ for ((i=1; i < ${#args[#]}; i++)); do out+=", VAL$i=${args[i]}"; done
$ echo "$out"
KEY=1, VAL1=2, VAL2=3, VAL3=4, VAL4=5

How to fetch last argument and stop before last arguments in shell script?

I want to merge all files into one. Here, the last argument is the destination file name.
I want to take last argument and then in loop stop before last arguments.
Here code is given that I want to implement:
echo "No. of Argument : $#"
for i in $* - 1
do
echo $i
cat $i >> last argument(file)
done
How to achieve that?
Using bash:
fname=${!#}
for a in "${#:1:$# - 1}"
do
echo "$a"
cat "$a" >>"$fname"
done
In bash, the last argument to a script is ${!#}. So, that is where we get the file name.
bash also allows selecting elements from an array. To start with a simple example, observe:
$ set -- a b c d e f
$ echo "${#}"
a b c d e f
$ echo "${#:2:4}"
b c d e
In our case, we want to select elements from the first to the second to last. The first is number 1. The last is number $#. We want to select all but the last. WE thus want $# - 1 elements of the array. Therefore, to select the arguments from the first to the second to last, we use:
${#:1:$# - 1}
A POSIX-compliant method:
eval last_arg=\$$#
while [ $# -ne 1 ]; do
echo "$1"
cat "$1" >> "$last_arg"
shift
done
Here, eval is safe, because you are only expanding a read-only parameter in the string that eval will execute. If you don't want to unset the positional parameters via shift, you can iterate over them, using a counter to break out of the loop early.
eval last_arg=\$$#
i=1
for arg in "$#"; do
echo "$arg"
cat "$arg" >> "$last_arg"
i=$((i+1))
if [ "$i" = "$#" ]; then
break
fi
done

counting how many time the order of two consecutive number in a file are reversed in a second file in BASH

I got the following problem:
Given 2 files of N numbers like
file1.dat: 1,2,3,4,5,6,7,8,9,0
file2.dat: 2,5,4,7,6,9,8,1,0,3
The file2.dat is actually just a shuffled version of the first one.
I want to know how many time the order of two consecutive numbers in the first file has changed in the second (that contain the same numbers). For exemple, in the file one we start looking for 1 and 2, in the second file 2 come before the 1, so there was a change of the order; in the first file there is 9 and then 0 and also in the second file this order is maintained because 9 come before 0.
The actual data that I'm using are roughly composed of 26000 numbers and they are all distinct.
I thought of somenthing like:
for(i=0; i<N-1; i++)
for(j=0; j<N; j++)
for(k=0 ; k<N; k++)
if(B[j]==A[i] && B[k]==A[i+1] && k < j )
count++
print("The number of inversion is: %d\n",count)
But I don't know how to write it in awk (I wrote a little C program but it takes more than 5 hours to give me the answer) and I don't know if something like could give me a result in a resonable time.
I hope your two files obey the certain rules:
same amount of distinct number,
both files have single line
I didn't do those format checking. see my solution:
awk -F, 'FNR==NR{n=NF;for(i=1;i<=NF;i++)o[i]=$i;next;}
{for(i=1;i<=NF;i++)v[$i]=i}
END{ for(i=1;i<=n-1;i++) t+=v[o[i]]>v[o[i+1]]?1:0;
print "inversions:",t;
}' file1 file2
test:
kent$ head file1 file2
==> file1 <==
1,2,3,4,5,6,7,8,9,0
==> file2 <==
2,5,4,7,6,9,8,1,0,3
kent$ awk -F, 'FNR==NR{n=NF;for(i=1;i<=NF;i++)o[i]=$i;next;}
{for(i=1;i<=NF;i++)v[$i]=i}
END{ for(i=1;i<=n-1;i++) t+=v[o[i]]>v[o[i+1]]?1:0;
print "inversions:",t;
}' file1 file2
inversions: 5
If you want to print some debug info, say, print inversion pairs as well, see this:
kent$ awk -F, 'FNR==NR{n=NF;for(i=1;i<=NF;i++)o[i]=$i;next;}
{for(i=1;i<=NF;i++)v[$i]=i}
END{ for(i=1;i<=n-1;i++) {
if(v[o[i]]>v[o[i+1]]){
print "inversion pair foud:"o[i],o[i+1]
t++;
}
}
print "inversions:",t;
}' file1 file2
inversion pair foud:1 2
inversion pair foud:3 4
inversion pair foud:4 5
inversion pair foud:6 7
inversion pair foud:8 9
inversions: 5
if you want some other information e.g. original index/order, changed order, they are also easy to be added.
hope it helps.
EDIT
if your data files are in single-column format. try this:
awk -F, 'FNR==NR{o[NR]=$0;next;}{v[$0]=FNR;n=FNR}
END{ for(i=1;i<=n-1;i++) t+=v[o[i]]>v[o[i+1]]?1:0;
print "invertions:",t;
}' file1 file2
test screencast. just for testing my just written recording script ;)
I know that you already have an accepted clean answer.
Just sharing:
sgeorge-mn:~ sgeorge$ cat stack.sh
VALUE1=$1
VALUE2=$2
for POS in `sed 's/,/ /g' file1.dat`
do
((COUNT++))
if [[ $VALUE1 == $POS ]] ; then
VAL1_POS=$COUNT
fi
if [[ $VALUE2 == $POS ]] ; then
VAL2_POS=$COUNT
fi
done
for MATCH in `sed 's/,/ /g' file2.dat`
do
((COUNT2++))
if [[ $VALUE1 == $MATCH ]] ; then
VAL1_POS2=$COUNT2
fi
if [[ $VALUE2 == $MATCH ]] ; then
VAL2_POS2=$COUNT2
fi
done
if [[ $VAL1_POS -gt $VAL2_POS ]] ; then
P1=1
fi
if [[ $VAL1_POS2 -gt $VAL2_POS2 ]] ; then
P2=1
fi
if [[ $VAL1_POS -lt $VAL2_POS ]] ; then
P1=2
fi
if [[ $VAL1_POS2 -lt $VAL2_POS2 ]] ; then
P2=2
fi
if [[ $VAL1_POS -eq $VAL2_POS ]] ; then
P1=3
fi
if [[ $VAL1_POS2 -eq $VAL2_POS2 ]] ; then
P2=3
fi
if [[ $P1 == $P2 ]]; then
echo "No order change"
else
echo "Order changed"
fi
How to execute the script:
I am assuming following:
Both file have exactly same numbers in same order.
You will not give non existing number (not existing in file*.dat) as input to the script
sgeorge-mn:~ sgeorge$ bash stack.sh 5 7
No order change
sgeorge-mn:~ sgeorge$ bash stack.sh 4 5
Order changed
sgeorge-mn:~ sgeorge$ bash stack.sh 9 0
No order change
sgeorge-mn:~ sgeorge$ bash stack.sh 1 2
Order changed

Filtering output of permutations using bash

I am really a Newbie in bash programing and I need to perform a permutation, which I did using one post from this forum as follow Generating permutations using bash.
#!/bin/bash
list=`echo {1..12}`
for c1 in $list
do
for c2 in $list
do
for c3 in $list
do
echo $c1-$c2-$c3
done
done
done
The output is
1-1-1
1-1-2
1-1-3
...
but I do not want to have a number repeated in the line (1-1-1).
Meaning if the number 1 is in the first position, I do not want it neither in the second nor in the third. like this
1-2-3
1-2-4
1-2-5
...
Can anybody help me? any hint is welcome.
I think you need to change your echo line to:
[ $c1 -ne $c2 -a $c1 -ne $c3 -a $c2 -ne $c3 ] && echo $c1-$c2-$c3
The link at the top of your question already includes a nice answer on how to do permutations in bash. But I think that's not the answer you're looking for so I suggest you to use the following script:
#!/bin/bash
list=`echo {1..12}`
for c1 in $list
do
for c2 in $list
do
if [ "$c1" != "$c2" ]; then
for c3 in $list
do
if [ "$c1" != "$c3" ]; then
echo $c1-$c2-$c3
fi
done
fi
done
done
Does this do what you're looking for?
#!/bin/bash
list=$(echo {1..12})
for c1 in $list
do
for c2 in $list
do
if (( c2 != c1 ))
then
for c3 in $list
do
if (( c3 != c2 && c3 != c1))
then
echo $c1-$c2-$c3
fi
done
fi
done
done
Partial output:
1-2-3
1-2-4
1-2-5
1-2-6
1-2-7
1-2-8
1-2-9
1-2-10
1-2-11
1-2-12
1-3-2
1-3-4
...
12-10-8
12-10-9
12-10-11
12-11-1
12-11-2
12-11-3
12-11-4
12-11-5
12-11-6
12-11-7
12-11-8
12-11-9
12-11-10
The solution in the questions gives a Cartesian product.
The following function generates the permutations of a set of values.
declare -a set=( 1 2 3 ) # set to permute
declare -i n=${#set[#]}
permute ()
{
declare -i k=$1
declare -i i
declare -i save
if [ $k -lt $((n-1)) ] ; then
for (( i=k; i<n; i+=1 )); do
save=${set[k]} # exchange elements
set[k]=${set[i]}
set[i]=$save
permute $((k+1)) # recurse
save=${set[k]} # exchange elements
set[k]=${set[i]}
set[i]=$save
done
else
(IFS='-'; echo -e "${set[*]}")
fi
} # ---------- end of function permute ----------
permute 0
The output:
1-2-3
1-3-2
2-1-3
2-3-1
3-2-1
3-1-2
I was having a similar problem but with text characters, so, just in case it may help:
for i in {a..z} ; do for b in {a..z} ; do [[ ! "$i" == "$b" ]] && echo -e "$i $b" | xargs -n 1 | sort | xargs ; done ; done | sort -u
It will permutate a to z with a to z, not only not repeating characters, "a a", but also without redundancies such as "ab" and "ba", echoing just "ab", thanks to the "inline sorting" of elements (xargs | sort | xargs) followed by the final "sort -u" (or "uniq").

Resources