Terminal script to group files into folders - shell

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 ?)

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

Bash: tr command with variables representing chars

I want to use the tr command to map chars to new chars, for example:
echo "hello" | tr '[a-z]' '[b-za-b]' Will output: ifmmp
(where each letter in the lower-case alphabet is shifted over one to the right)
See below the mapping to new chars for '[b-za-b]':
[a b c d e f g h i j k l m n o p q r s t u v w x y z] will map to:
[b c d e f g h i j k l m n o p q r s t u v w x y z a]
However, when I want it to rotate multiple times, how can I use a variable to control the rotate-value for the tr command?
Eg: for a shift of 1:
echo "hello" | tr '[a-z]' '[b-za-b]' without variables and:
echo "hello" | tr '[a-z]' '[(a+$var)-za-(a+$var)]' where $var=1
here I have: (a+$var)-z representing the same as b-z and
....................a-(a+$var) representing the same as a-b
I have tried converting the ascii value to a char to use within the tr command but I don't think that is allowed.
My problem is that bash is not interpreting:
(a+$var) as the char b when $var=1
(a+$var) as the char c when $var=2
... etc.
How can I tell bash to interpret these equations as chars for the tr command
EDIT
I tried doing it with an array but it's not working:
chars=( {a..z} )
var=2
echo "hello" | tr '[a-z]' '[(${chars[var]}-z)(a-${chars[var]})]'
I used: (${chars[var]}-z) to represent b-z where var=1
Because ${chars[1]} is b but this is not working. Am I missing something?
What you are trying to do cannot be done using tr which does not handle your requirement. Moreover when you meant to modify and use variables to add to glob patterns in bash which is something you cannot possibly do.
There is a neat little trick you can do with bash arrays!. The tr command can take expanded array sequence over the plain glob patterns also. First define a source array as
source=()
Now add its contents as a list of character ranges from a-z using brace expansion as
source=({a..z})
and now for the transliterating array, from the source array, construct it as follows by using the indices to print the array elements
trans=()
Using a trick to get the array elements from the last with syntax ${array[#]: (-num)} will get you the total length - num of the elements. So building the array first as
var=2
trans+=( "${source[#]:(-(26-$var))}" )
and now to build the second part of the array, use another trick ${array[#]:0:num} to get the first num number of elemtents.
trans+=( "${source[#]:0:$(( $var + 1 ))}" )
So what we have done now is for a given value of var=2, we built the trans array as
echo "${trans[#]}"
c d e f g h i j k l m n o p q r s t u v w x y z a b c
Now you can just use it easily in the tr command
echo "hello" | tr "${source[*]}" "${trans[*]}"
jgnnq
You can just put it all in function and print its value as
transChar() {
local source
local trans
local result
source=({a..z})
trans=()
var="$2"
input="$1"
trans+=( "${source[#]:(-(26-$var))}" )
trans+=( "${source[#]:0:$(( $var + 1 ))}" )
result=$( echo "$input" | tr "${source[*]}" "${trans[*]}" )
echo "$result"
}
Some of the tests
transChar "hello" 1
ifmmp
transChar "hello" 2
jgnnq
transChar "hello" 3
khoor
rot-random:
# generate alphabet as arr:
arr=( {1..26} )
i=$(($RANDOM%24+1))
# left and right
l=$(echo ${arr[$i]})
r=$(echo ${arr[$i+1]})
# reusing arr for testing:
echo ${arr[#]} | tr "a-z" "$r-za-$l"
echo "secret:" | tr "a-z" "$r-za-$l" ; echo $l $r $i
amkzmb:
h i 7
You could use octal \XXX character codes for characters to do what you intend. Using the octal codes you could do any arithmetic manipulations to numbers and then convert them to character codes
# rotr x
#
# if 0 <= x <= 25 rotr x outputs a set specification
# that could be used as an argument to tr command
# otherwise it outputs 'a-z'
function rotr(){
i = $(( 97 + $1 ))
if [ $i -lt 97 ] ; then
translation='a-z'
elif [ $i -eq 97 ] ; then
translation='\141-\172' # 141 is the octal code for "a"
# 172 is the octal code for "z"
elif [ $i -eq 98 ] ; then
translation='\142-\172\141'
elif [ $i -lt 122 ] ; then # $i is between 99 and 121 ("c" and "y")
ii=$(echo "obase=8 ; $i" | bc)
jj=$(echo "obase=8 ; $(( $i - 1 ))" | bc)
translation=\\$ii'-\172\141-'\\$jj
elif [ $i -eq 122 ] ; then
translation='\172\141-\171'
else # $i > 122
tranlation='a-z'
fi
echo $translation
}
Now you could use this as follows
echo hello | tr 'a-z' $(rotr 7)
prints
olssv

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 Loop Help (want to echo aa, bb, cc)

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

while loops offset in shell script

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

Resources