Destroy Hierarchy of directory? - bash

I have a folder with many folders in it and many folders below that and so on and so forth. In those final folders are are small clusters of files. I am attempting to move those files to the main folder and delete the now empty folder hierarchy. This is what I have so far.
#!/bin/bash
NAME=`whoami`
DEST="/Users/"$NAME"/Desktop/Music 2"
FILES=`find "$DEST" -type f`
for F in "$FILES"
do
mv "${F}" "${DEST}"
done
If I replace the mv command with "echo" it will catch all the right names but when I run this it gives me an error saying that the name is too long. Help will be greatly appreciated.
So say I have
/foo/bar/in/side/test1.txt
/foo/bar/in/down/test2.doc
/foo/bar/last/dog/test3.mp3
I want test1.txt, test2.doc, and test3.mp3 to be in /foo, and for each of the (now empty) directories /foo/bar, /foo/bar/in, /foo/bar/in/side, /foo/bar/in/down, /foo/bar/last, and /foo/bar/last/dog to be deleted.
End result:
/foo/test1.txt
/foo/test2.doc
/foo/test3.mp3

Try doing this :
find "$DEST" -type f -exec bash -c '
mv "$1" "$DEST"; rmdir "${1%/*}" &>/dev/null
' -- {} \;

Especially when the path names have spaces in them, using FILES=$(find ...) really doesn't work. You got the file name too long message because "$FILES" in the for loop treats all the names as a single file name; ${F} contains everything, and the mv command is trying to move a single file to ${DEST}.
The rmdir -p command removes directories that are empty (think of -p for 'prune'), working depth first.
GNU mv has a very useful option -t target for use in this context:
DEST=/foo
find /foo/bar -type f -exec mv -t "${DEST}" {} +
find /foo/bar -type d -depth -exec rmdir -p {} +
Given that you're on Mac OS X, you don't have quite that convenience, so your best bet is a slower (but equally effective):
DEST=/foo
find /foo/bar -type f -exec mv {} "${DEST}" ';'
find /foo/bar -type d -depth -exec rmdir -p {} +
This executes the mv command once per file (whereas with GNU mv, many files may be moved with a single invocation). Otherwise, it is equivalent.
Both sets of commands avoid the issues with spaces in file names.
Note that if $DEST is the same as the directory you're searching in, you'll run into problems moving the files already in $DEST over themselves. As written, the code does not avoid that problem. If necessary, you can avoid that with:
find "$DEST"/*/ -type f ...
The trailing slash enforces 'only directories' (think of it as equivalent to "$DEST"/*/.).
Proof of concept script
Remember: always test destructive scripts (scripts that delete stuff) on copies of the live material, unless you've got good backups on hand. Actually, make them on copies anyway; it is almost invariably quicker to make a copy than to recover from a backup (but you should have a backup anyway if the data is crucial).
echo "Before"
du -a .
filelist="./foo/bar/in/side/test1.txt
./foo/bar/in/down/test2.doc
./foo/bar/last/dog/test3.mp3"
for file in $filelist
do
mkdir -p $(dirname $file)
cp script $file
done
FIFO=./foo/bar/first/installment
mkdir $(dirname $FIFO)
mkfifo $FIFO
echo "Created"
du -a .
echo "Clean up"
DEST=./foo
find ./foo/bar -type f -exec mv {} "${DEST}" ';'
find ./foo/bar -depth -type d -exec rmdir -p {} + 2>/dev/null
echo "After"
du -a .
rm -fr ./foo
The rmdir -p process is noisy. It reports on directories it can't remove. The GNU version of rmdir provides an option to suppress some errors (--ignore-fail-on-non-empty), but in the context, you end up with some errors about non-existent directories, too (they were removed by the pruning process before the entry that listed the directory on its own). So, after putting up with the noise for a bit, I redirected all errors from rmdir to /dev/null. Remove that redirection until you're satisfied things work as intended.
This script should be run in an empty directory you've just created and made your current directory:
mkdir junk
cd junk
cp ../script .
sh -x ./script
Sample output:
$ sh -x script
+ echo Before
Before
+ du -a .
4 ./script
8 .
+ filelist='./foo/bar/in/side/test1.txt
./foo/bar/in/down/test2.doc
./foo/bar/last/dog/test3.mp3'
+ for file in '$filelist'
++ dirname ./foo/bar/in/side/test1.txt
+ mkdir -p ./foo/bar/in/side
+ cp script ./foo/bar/in/side/test1.txt
+ for file in '$filelist'
++ dirname ./foo/bar/in/down/test2.doc
+ mkdir -p ./foo/bar/in/down
+ cp script ./foo/bar/in/down/test2.doc
+ for file in '$filelist'
++ dirname ./foo/bar/last/dog/test3.mp3
+ mkdir -p ./foo/bar/last/dog
+ cp script ./foo/bar/last/dog/test3.mp3
+ FIFO=./foo/bar/first/installment
++ dirname ./foo/bar/first/installment
+ mkdir ./foo/bar/first
+ mkfifo ./foo/bar/first/installment
+ echo Created
Created
+ du -a .
4 ./foo/bar/in/side/test1.txt
8 ./foo/bar/in/side
4 ./foo/bar/in/down/test2.doc
8 ./foo/bar/in/down
20 ./foo/bar/in
4 ./foo/bar/last/dog/test3.mp3
8 ./foo/bar/last/dog
12 ./foo/bar/last
0 ./foo/bar/first/installment
4 ./foo/bar/first
40 ./foo/bar
44 ./foo
4 ./script
52 .
+ echo 'Clean up'
Clean up
+ DEST=./foo
+ find ./foo/bar -type f -exec mv '{}' ./foo ';'
+ find ./foo/bar -depth -type d -exec rmdir -p '{}' +
+ echo After
After
+ du -a .
0 ./foo/bar/first/installment
4 ./foo/bar/first
8 ./foo/bar
4 ./foo/test1.txt
4 ./foo/test3.mp3
4 ./foo/test2.doc
24 ./foo
4 ./script
32 .
+ rm -fr ./foo
Note that this script carefully creates a non-file (a FIFO) in a separate directory under ./foo/bar and shows that it is left behind. Comment out the mkfifo line that create the FIFO and the run looks like:
$ sh -x script
+ echo Before
Before
+ du -a .
4 ./script
8 .
+ filelist='./foo/bar/in/side/test1.txt
./foo/bar/in/down/test2.doc
./foo/bar/last/dog/test3.mp3'
+ for file in '$filelist'
++ dirname ./foo/bar/in/side/test1.txt
+ mkdir -p ./foo/bar/in/side
+ cp script ./foo/bar/in/side/test1.txt
+ for file in '$filelist'
++ dirname ./foo/bar/in/down/test2.doc
+ mkdir -p ./foo/bar/in/down
+ cp script ./foo/bar/in/down/test2.doc
+ for file in '$filelist'
++ dirname ./foo/bar/last/dog/test3.mp3
+ mkdir -p ./foo/bar/last/dog
+ cp script ./foo/bar/last/dog/test3.mp3
+ FIFO=./foo/bar/first/installment
++ dirname ./foo/bar/first/installment
+ mkdir ./foo/bar/first
+ echo Created
Created
+ du -a .
4 ./foo/bar/in/side/test1.txt
8 ./foo/bar/in/side
4 ./foo/bar/in/down/test2.doc
8 ./foo/bar/in/down
20 ./foo/bar/in
4 ./foo/bar/last/dog/test3.mp3
8 ./foo/bar/last/dog
12 ./foo/bar/last
4 ./foo/bar/first
40 ./foo/bar
44 ./foo
4 ./script
52 .
+ echo 'Clean up'
Clean up
+ DEST=./foo
+ find ./foo/bar -type f -exec mv '{}' ./foo ';'
+ find ./foo/bar -depth -type d -exec rmdir -p '{}' +
+ echo After
After
+ du -a .
4 ./foo/test1.txt
4 ./foo/test3.mp3
4 ./foo/test2.doc
16 ./foo
4 ./script
24 .
+ rm -fr ./foo
$
This strongly suggests that if written properly and handled carefully, the code above does work correctly without damaging the system. But you should still be cautious before using any variant of this in production (and even in test).
Tests run on an Ubuntu 12.04 derivative.

Related

Sort files based on filename into folders and concat files within each folder based on folder name

Any help would be VERY appreciated! I have hundreds of video files named in the following format (see below). The first 4 characters are random, but there is always 4. 3000 is always there.
Can someone please help me create folders based on the center of the filename (ie 000, 001, 002, 003 and so on).
Then concatenate all the files in each of the folders using ffmpeg in order in their filename. 0000.ts, 0001.ts, 0002.ts and so on to a file named 000merged.ts, 001merged.ts, 002merged.ts and so on...
This is close to what I need
find . -type f -name "*jpg" -maxdepth 1 -exec bash -c 'mkdir -p "${0%%_*}"' {} \; \
-exec bash -c 'mv "$0" "${0%%_*}"' {} ;
mkdir /tmp/test && cd $_ #or cd ~/Desktop
echo A > 1e98_3000_000_000_0000.ts #create some small test files
echo B > 1e98_3000_000_000_0001.ts
echo C > 1e98_3000_000_000_0002.ts
echo D > 1e98_3000_000_000_0003.ts
echo E > d82j_3000_001_000_0000.ts
echo F > d82j_3000_001_000_0001.ts
echo G > d82j_3000_001_000_0002.ts
echo H > d82j_3000_001_000_0003.ts
echo I > a03l_3000_002_000_0000.ts
echo J > a03l_3000_002_000_0001.ts
echo K > a03l_3000_002_000_0002.ts
echo L > a03l_3000_002_000_0003.ts
# mkdir and copy each *.ts into its dir plus rename file:
perl -E'/^...._3000_(...)_..._(....\.ts)$/&&qx(mkdir -p $1;cp -p $_ $1/$2)for#ARGV' *.ts
ls -rtl
find ??? -type f -ls
for dir in ???;do cat $dir/????.ts > $dir/${dir}merged.ts; done
ls -rtl */*merged.ts
Cleanup test:
rm -rf /tmp/test/??? #cleanup new dirs with files
rm -rf /tmp/test #cleanup all

bash loop in parallel

I am trying to run this script in parallel, for i<=4 in each set. The runspr.py is itself parallel, and thats fine. What I am trying to do is running only 4 i loop in any instance.
In my present code, it will run everything.
#!bin/bash
for i in *
do
if [[ -d $i ]]; then
echo "$i id dir"
cd $i
python3 ~/bin/runspr.py SCF &
cd ..
else
echo "$i nont dir"
fi
done
I have followed https://www.biostars.org/p/63816/ and https://unix.stackexchange.com/questions/35416/four-tasks-in-parallel-how-do-i-do-that
but unable to impliment the code in parallel.
You don't need to use for loop. You can use gnu parallel like this with find:
find . -mindepth 1 -maxdepth 1 -type d ! -print0 |
parallel -0 --jobs 4 'cd {}; python3 ~/bin/runspr.py SCF'
Another possible solution is:
find . -mindepth 1 -maxdepth 1 -type d ! -print0 |
xargs -I {} -P 4 sh -c 'cd {}; python3 ~/bin/runspr.py SCF'

Bash - create subfolders for folders recently created

I want to "join" these two tasks:
for dir in /blabla/bleble/*; do (cd "$dir" && mkdir -p Folder1/Folder1a && mkdir -p Folder2); done
and
find -amin -10
How can I do this?
I've tried this, but it doesn't work:
find -amin -2 -exec sh -c '
for dir in /blabla/bleble/*; do (cd "$dir" && mkdir -p Folder1/Folder1a && mkdir -p Folder2);
done' sh {} +
Something like this is might do:
find /a/b/ -mindepth 1 -maxdepth 1 -type d -amin -2 \
-exec sh -c 'for f; do mkdir -p -- "$f/Folder1/Folder1a" "$f/Folder2; done"' "" {} +
Breakdown:
-mindepth 1 -maxdepth 1 will limit result to current directory only.
-type d makes sure you only list directories
-exec foo "" {} + will execute foo with matches as arguments:
foo "" "/a/b/c" "/a/b/john" "a/b/doe"
for f will iterate over all positional arguments ($1, $2, ...)
for f; do
mkdir -p -- "$f/Folder1/Folder1a" "$f/Folder2"
done
Running sh -c 'code' arg1 arg2 will set $0 to arg1, and $1 to arg2, therefore the empty argument: foo "" {} +:
% sh -c 'echo $0' john
john
Assuming there aren't so many folders in /blabla/bleble that you overflow the command line, you can use find to search the target directory. -prune prevents recursing into the directories.
find /blabla/bleble/* -prune -type d -amin -10 -exec mkdir -p {}/Folder1/Folder1a {}/Folder2 \;
If you are using GNU find or another version that supports them, use -mindepth and -maxdepth instead to find the top-level subdirectories, no matter how many there are.
find /blabla/bleble -maxdepth 1 -mindepth 1 -type d -amin -10 -exec mkdir -p {}/Folder1/Folder1a {}/Folder2 \;

BASH script more smart with cat

I have multiple files in multiple folders
[tiagocastro#cascudo clean_reads]$ ls
11 13 14 16 17 18 3 4 5 6 8 9
and I want to make a tiny bash script to concatenate these files inside :
11]$ ls
FCC4UE9ACXX-HUMcqqTAAFRAAPEI-206_L6_1.fq FCC4UE9ACXX-HUMcqqTAAFRAAPEI-206_L7_1.fq
FCC4UE9ACXX-HUMcqqTAAFRAAPEI-206_L6_2.fq FCC4UE9ACXX-HUMcqqTAAFRAAPEI-206_L7_2.fq
But only L6 with L6 and L7 with L7
Right now I am on the basic level. I want to learn how to do it more smartly, instead of reproduce the commands I could do in terminal in the script.
Thank you everybody, for helping me.
This isn't an free programmiing service, but you can learn something from the next:
#!/bin/bash
echo2() { echo "$#" >&2; }
get_Lnums() {
find . -type f -regextype posix-extended -iregex '.*_L[0-9]+_[0-9]+\.fq' -maxdepth 1 -print | grep -oP '_\KL\d+' | sort -u
}
docat() {
echo2 doing $(pwd)
for lnum in $(get_Lnums)
do
echo cat *_${lnum}_*.fq "> new_${lnum}.all" #remove (comment out) this line when satisfied
#cat *_${lnum}_*.fq > new_${lnum}.all #and uncomment this
done
}
while read -r -d $'\0' dir
do
(cd "$dir" && docat) #subshell - don't need cd back
done < <(find . -type dir -maxdepth 1 -mindepth 1 -print0)

Perform an action in every sub-directory using Bash

I am working on a script that needs to perform an action in every sub-directory of a specific folder.
What is the most efficient way to write that?
A version that avoids creating a sub-process:
for D in *; do
if [ -d "${D}" ]; then
echo "${D}" # your processing here
fi
done
Or, if your action is a single command, this is more concise:
for D in *; do [ -d "${D}" ] && my_command; done
Or an even more concise version (thanks #enzotib). Note that in this version each value of D will have a trailing slash:
for D in */; do my_command; done
for D in `find . -type d`
do
//Do whatever you need with D
done
The simplest non recursive way is:
for d in */; do
echo "$d"
done
The / at the end tells, use directories only.
There is no need for
find
awk
...
Use find command.
In GNU find, you can use -execdir parameter:
find . -type d -execdir realpath "{}" ';'
or by using -exec parameter:
find . -type d -exec sh -c 'cd -P "$0" && pwd -P' {} \;
or with xargs command:
find . -type d -print0 | xargs -0 -L1 sh -c 'cd "$0" && pwd && echo Do stuff'
Or using for loop:
for d in */; { echo "$d"; }
For recursivity try extended globbing (**/) instead (enable by: shopt -s extglob).
For more examples, see: How to go to each directory and execute a command? at SO
Handy one-liners
for D in *; do echo "$D"; done
for D in *; do find "$D" -type d; done ### Option A
find * -type d ### Option B
Option A is correct for folders with spaces in between. Also, generally faster since it doesn't print each word in a folder name as a separate entity.
# Option A
$ time for D in ./big_dir/*; do find "$D" -type d > /dev/null; done
real 0m0.327s
user 0m0.084s
sys 0m0.236s
# Option B
$ time for D in `find ./big_dir/* -type d`; do echo "$D" > /dev/null; done
real 0m0.787s
user 0m0.484s
sys 0m0.308s
find . -type d -print0 | xargs -0 -n 1 my_command
This will create a subshell (which means that variable values will be lost when the while loop exits):
find . -type d | while read -r dir
do
something
done
This won't:
while read -r dir
do
something
done < <(find . -type d)
Either one will work if there are spaces in directory names.
You could try:
#!/bin/bash
### $1 == the first args to this script
### usage: script.sh /path/to/dir/
for f in `find . -maxdepth 1 -mindepth 1 -type d`; do
cd "$f"
<your job here>
done
or similar...
Explanation:
find . -maxdepth 1 -mindepth 1 -type d :
Only find directories with a maximum recursive depth of 1 (only the subdirectories of $1) and minimum depth of 1 (excludes current folder .)
the accepted answer will break on white spaces if the directory names have them, and the preferred syntax is $() for bash/ksh. Use GNU find -exec option with +; eg
find .... -exec mycommand +; #this is same as passing to xargs
or use a while loop
find .... | while read -r D
do
# use variable `D` or whatever variable name you defined instead here
done
if you want to perform an action INSIDE the folder and not ON folder.
Explanation: You have many pdfs and you would like to concetrate them inside a single folder.
my folders
AV 001/
AV 002/
for D in *; do cd "$D"; # VERY
DANGEROUS COMMAND - DONT USE
#-- missing "", it will list files too. It can go up too.
for d in */; do cd "$d"; echo $d; cd ..; done; # works
succesfully
for D in "$(ls -d */)"; do cd "$D"; done; #
bash: cd: $'Athens Voice 001/\nAthens Voice 002/' - there is no such
folder
for D in "$(*/)"; do cd "$D"; done; # bash: Athens
Voice 001/: is folder
for D in "$(`find . -type d`)"; do cd $D; done; # bash: ./Athens: there is no such folder or file
for D in *; do if [ -d "${D}" ] then cd ${D}; done; # many
arguments

Resources