I have a bunch of .mov video in a folder on my mac:
Documents/MyMovs
├── SubFolder01
│ ├── SubSubFolder01
│ │ ├── MyFile1.mov
│ │ ├── MyPdf.pdf
│ │ └── MyTxt.txt
│ └── SubSubFolder02
│ ├── MyFile2.mov
│ ├── MyPdf.pdf
│ └── MyTxt.txt
etc.
Using HandBrake I converted all of them to .mp4 but HandBrake put them all in Movies/
Movies
├── MyFile1.mp4
├── MyFile2.mp4
etc.
What I want to do is swap the files and get the exact reverse situation where all my mp4 are stored (at the correct location) in Documents/MyMovs and all mov files are in Movies/ where I will be able to delete them eventually.
at the end I want:
Documents/MyMovs
├── SubFolder01
│ ├── SubSubFolder01
│ │ ├── MyFile1.mp4
│ │ ├── MyPdf.pdf
│ │ └── MyTxt.txt
│ └── SubSubFolder02
│ ├── MyFile2.mp4
│ ├── MyPdf.pdf
│ └── MyTxt.txt
etc.
and
Movies
├── MyFile1.mov
├── MyFile2.mov
etc.
I've designed the following bash script to help me (although I have the time to do it by hand due to the coronavirus crisis):
#!/usr/local/bin/bash
MOV_DIR=$1
MP4_DIR=$2
declare -A FILES=( )
find $MOV_DIR -maxdepth 3 -name '*.mov' -print0 |
while IFS= read -r -d '' line; do
filename=$(basename "$line")
extension="${filename##*.}"
filename="${filename%.*}"
path_to_file=$(dirname "$line")
if [[ ! -v FILES[$filename] ]]; then
FILES[$filename]="$path_to_file"
#echo $FILES[$filename] prints something!
fi
done
#for k in "${!FILES[#]}"; do echo "$k - ${FILES[$k]}"; done ## prints nothing!
find $MP4_DIR -maxdepth 3 -name '*.mp4' -print0 |
while IFS= read -r -d '' line; do
filename=$(basename "$line")
extension="${filename##*.}"
filename="${filename%.*}"
path_to_file=$(dirname "$line")
if [[ ! -z ${FILES["$filename"]} ]]; then
mv "$path_to_file/$filename.mp4" "${FILES[$filename]}"
mv "${FILES[$filename]}/filename.mov" "$path_to_file"
#I would like to remove $filename from FILES as well but I don't know how
fi
done
Let's assume all filenames are unique and distinct from each other for simplicity.
I put all my mov files in a map filename -> path_to_filename; for example MyFile1 -> Documents/MyMovs/SubFolder01/SubSubFolder01 . This is the first find block.
Secondly I find all my mp4 files and retrieve the correct location using the map.
The code does not work it seems the map is local to the first find command and does not exist after. Would anybody know why? I use bash version 5.0.16(1)-release. Thanks everybody!
PS: I you have a completely different but better solution feel free to share
If you have one file, then find the other
find Documents -maxdepth 4 -mindepth 3 -name '*.mov' -print0 |
while IFS= read -r -d '' mov; do
# extract the filename without extension
name=$(basename "$mov" .mov)
# find exactly the same name in Movies but with mp4
mp4="Movies/$name.mp4"
if [[ ! -f "$mp4" ]]; then
echo "ERROR: File $mp4 does not exists" >&2
exit 2
fi
# switch mp4 with mov
movdir=$(dirname "$mov")
mp4dir=$(dirname "$mp4")
echo mv -v "$mp4" "$movdir"
echo mv -v "$mov" "$mp4dir"
done
With the following directory/file recreation:
# find -type f
./Documents/MyMovs/Subfolder01/SubSubFolder01/MyFile1.mov
./Documents/MyMovs/Subfolder01/SubSubFolder02/MyFile2.mov
./Movies/MyFile1.mp4
./Movies/MyFile2.mp4
outputs:
mv -v Movies/MyFile1.mp4 Documents/MyMovs/Subfolder01/SubSubFolder01
mv -v Documents/MyMovs/Subfolder01/SubSubFolder01/MyFile1.mov Movies
mv -v Movies/MyFile2.mp4 Documents/MyMovs/Subfolder01/SubSubFolder02
mv -v Documents/MyMovs/Subfolder01/SubSubFolder02/MyFile2.mov Movies
does not exist after. Would anybody know why?
Because the right side of | is run in a subshell. Parent shell doesn't know anything about child shell environment.
$ echo | { a=1; echo a=$a; }
a=1
$ echo a=$a
a=
Related
I am trying to take a list of names from a text file and compare them with a list of directories. If there is a match in the directories then move them.
The code below doesn't work but it is essentially what I am trying to achieve.
#!/bin/bash
echo "Starting"
names="names.txt"
while IFS= read -r directory; do
find 'Folder/' -type d -name '$directory' -print0
done < "$names" | xargs -t mv Folder/ MoveTo/
Example folder structure:
Folder/
folder1
folder2
folder3
oddfolder
oddfolder2
MoveTo/
(empty)
Example text file structure:
folder1
folder2
folder3
Output expectation:
Folder/
oddfolder
oddfolder2
MoveTo/
folder1
folder2
folder3
I don't have an issue with spaces or capitalization. If there is a match then I want to move the selected folders to a different folder.
This should work:
$ tree
├── folder
│ ├── f1
│ ├── f2
│ ├── f3
│ ├── f4
│ ├── other1
│ └── other2
├── name.txt
└── newdir
$ cat name.txt
f1
f2
f3
f4
$ while IFS= read -r dir; do
mv "folder/$dir" newdir/. 2>/dev/null
done < name.txt
$ tree
.
├── folder
│ ├── other1
│ └── other2
├── name.txt
└── newdir
├── f1
├── f2
├── f3
└── f4
Note that you should also use " instead of ' with variable
You do not have to execute find command within the while loop.
The test [[ -d dirname ]] will be enough to confirm the existence
of the directory. Would you please try:
#!/bin/bash
names="names.txt"
src="Folder"
dest="MoveTo"
while IFS= read -r dir; do
[[ -d $src/$dir ]] && mv "$src/$dir" "$dest"
done < "$names"
Let's suppose there is a folder with several subfolders. In each subfolder there is a file, that has a different name depending on the folder. For example
basefolder
|________f1_1_1: video_1_1_1.mp4
|________f1_2_1: video_1_2_1.mp4
|
|_ .....
I want to write a shell script that do some processing on these files
So I have
search_dir=/path/to/the/basefolder/
for entry in "$search_dir"*/
do
echo "$entry"
#ls "$entry" #<--------HERE
echo "========================"
done
As you can see I can list the subfolders.
I want to do something like
process video_1_1_1.mp4 video_1_1_1_out.mp4
but the file name varies.
Yes I see that I can perhaps use the entry variable to compose the name of the file, but what if the files don't follow this pattern and the only thing I know is that they start with "video"?
Is there a way to get the name of the file in the folder so as to use it later?
Consider this file tree:
$ tree /tmp/test
/tmp/test
├── one
│ ├── one-1.mp4
│ ├── one-2.mp4
│ ├── one-3.mp4
│ ├── video-1.mp4
│ └── video-2.mp4
└── two
├── two-1.mp4
├── two-2.mp4
├── two-3.mp4
├── video-1.mp4
└── video-2.mp4
2 directories, 10 files
You can use a recursive glob to find all the .mp4 files in that tree:
$ for fn in "/tmp/test/"**/*".mp4"; do echo "$fn"; done
/tmp/test/one/one-1.mp4
/tmp/test/one/one-2.mp4
/tmp/test/one/one-3.mp4
/tmp/test/one/video-1.mp4
/tmp/test/one/video-2.mp4
/tmp/test/two/two-1.mp4
/tmp/test/two/two-2.mp4
/tmp/test/two/two-3.mp4
/tmp/test/two/video-1.mp4
/tmp/test/two/video-2.mp4
Or just the ones starting with video:
$ for fn in "/tmp/test/"**/"video-"*".mp4"; do echo "$fn"; done
/tmp/test/one/video-1.mp4
/tmp/test/one/video-2.mp4
/tmp/test/two/video-1.mp4
/tmp/test/two/video-2.mp4
Instead of echo you can process...
If process involves more than one file, you can use xargs.
You can also use find:
$ find "/tmp/test/" -iname "video*.mp4" -type f
/tmp/test//one/video-1.mp4
/tmp/test//one/video-2.mp4
/tmp/test//two/video-1.mp4
/tmp/test//two/video-2.mp4
Then you would construct a pipe to xargs or use find -exec:
$ find [ what ] -print0 | xargs -0 process # xargs way
$ find [ what ] -exec process {} + # modern find
I am trying to delete a set of characters like single quote (') and spaces from file names and directories. Example, I have:
Directory I'm confused which contains file you're right
So far, I have been able to create a short script:
#!/bin/sh
for f in *; do mv "$f" `echo $f | tr ' ' '_'`; done
for f in *; do mv "$f" `echo $f | tr -d \'`; done
which renames the dir to Im_confused as intended. The file in the directory of course is not affected.
How can I replace and delete characters in subdirectories as well?
For example, for depth 2, the command is:
REP_CHARS=" →" # Characters to replace
DEL_CHARS="'," # Characters to delete
find . -maxdepth 2 | sort -r |
sed -n -e '/^\.\+$/!{p;s#.\+/#&\n#;p}' |
sed "n;n;s/[$DEL_CHARS]//g;s/[$REP_CHARS]/_/g" |
sed "n;N;s/\n//" |
xargs -L 2 -d '\n' mv 2>/dev/null
Use find with -maxdepth.
Use sort to order from the deepest.
Use sed to replace only the end part.
Use xargs to perform mv.
[Original]
├── I'm confused
│ ├── I'm confused
│ │ └── you're right
│ ├── comma, comma
│ └── you're right
└── talking heads-love → building on fire
└── talking heads-love → building on fire
[After]
├── Im_confused
│ ├── Im_confused
│ │ └── you're right
│ ├── comma_comma
│ └── youre_right
└── talking_heads-love___building_on_fire
└── talking_heads-love___building_on_fire
I would use this rename script:
#!/bin/sh
for f in *; do
g=$(printf '%s' "$f" | tr -s '[:space:]' _ | tr -d "'")
[ "$f" != "$g" ] && mv -v "$f" "$g"
done
and this find invocation
find . -depth -execdir /absolute/path/to/rename.sh '{}' +
-depth does a depth-first descent into the file hierarchy so the files are renamed before their parent directories
-execdir performs the command in the directory where the file is found, so the value of $f only contains the filename not its directory as well.
Demo
$ mkdir -p "a b/c d/e f"
$ touch a\ b/c\ d/e\ f/"I'm confused"
$ touch "a file with spaces"
$ tree
.
├── a\ b
│ └── c\ d
│ └── e\ f
│ └── I'm\ confused
├── a\ file\ with\ spaces
└── rename.sh
3 directories, 3 files
$ find . -depth -execdir /tmp/rename.sh '{}' +
renamed 'a b' -> 'a_b'
renamed 'a file with spaces' -> 'a_file_with_spaces'
renamed "I'm confused" -> 'Im_confused'
renamed 'e f' -> 'e_f'
renamed 'c d' -> 'c_d'
$ tree
.
├── a_b
│ └── c_d
│ └── e_f
│ └── Im_confused
├── a_file_with_spaces
└── rename.sh
3 directories, 3 files
I am trying to mass rename folders and their respective files by use of the command "mv" but its not going very well, I would like the folders to be renamed into:
1/
2/
3/
etc.
The files in each folder should in this process also be renamed while keeping their file extension.
1.png
2.gif
3.jpg
etc
Very thankful for your help
EDIT: Better approach with a function rather than recursive calls to the same script.
Here's hoping I got all corner cases. This descends into directories recursively to handle deeply nested directory trees.
Caveat: Because the script takes care to not overwrite existing files, gaps may appear in the numbering in some corner cases -- if there is a file 0.txt in a directory and the first file that is handled in that directory is a .txt file, it will be moved to 1.txt. This also happens if the first file that is handled is 0.txt, so running the script twice will change the numbering, and running it again will change it back.
So here's the code:
#!/bin/bash
handle_directory() {
local counter=0
for i in *; do
# if the file is a directory (but not a symlink),
# handle it before moving
if [ -d "$i" ] && ! [ -h "$i" ]; then
cd "$i"
handle_directory
cd ..
fi
# extract suffix
suffix="${i##*.}"
if [ "$suffix" != "$i" ]; then
extension=".$suffix"
else
# If there is no filename extension, the counter
# is the whole filename. Without this, we'd get
# 0.Makefile and suchlike.
extension=""
fi
# find a filename that isn't already taken
# this may lead to gaps in the numbering.
while dest="$counter$extension" && [ -e "$dest" ]; do
let ++counter
done
echo mv "$i" "$dest"
let ++counter
done
}
# if a parameter was given, go there to handle it.
# otherwise handle the local directory.
if ! [ -z "$1" ] && ! cd "$1"; then
echo "Could not chdir to directory $1"
exit -1
fi
handle_directory
The general idea is a depth-first search of the directory tree in question. Like any tree, the directory tree is best handled recursively, and the function essentially boils down to: Walk through all things in this directory, if they are a directory, descend and handle it, then find an appropriate file name and rename the thing whether it is a directory or not.
Things used:
local counter=0 # declares a function-local variable counter and initializes it
# to 0. crucially, that it is local means that every invocation
# of handle_directory has its own counter.
[ -d "$i" ] # tests if "$i" is a directory
[ -h "$i" ] # tests if "$i" is a symlink
! [ ... ] # negates the test. ! [ -h "$i" ] is true if "$i" is NOT a symlink
"${i##*.}" # a bashism that cuts off the longest prefix that matches the pattern
[ -e "$dest" ] # tests if "$dest" exists
$1 # the first parameter with which the script is called
[ -z "$1" ] # tests if "$1" is an empty string
! cd "$1" # descends into the directory "$1". true if that failed.
Manpages to read to further understanding:
man test
man bash # that's the big one.
Is it a single level directory? Or do you have subdirectories?
Something like this?
Before:
.
├── folderE
│ ├── conf.properties
│ ├── omg.avi
│ └── test-diff.diff
├── folder_a
│ ├── first.png
│ ├── main.jpg
│ └── second.gif
├── folder_d
│ ├── another.zip
│ └── one.mpeg
└── with space
└── file with spaces too.mov
Command:
countDir=1
for dir in *; do
cd "$dir";
countFile=1
for file in *; do
mv "$file" $countFile.${file#*.}
((countFile++))
done
cd ..
mv "$dir" $countDir
((countDir++))
done
Or, the same in one line:
countDir=1; for dir in *; do cd "$dir"; countFile=1; for file in *; do mv "$file" $countFile.${file#*.}; ((countFile++)); done; cd ..; mv "$dir" $countDir; ((countDir++)); done
After:
.
├── 1
│ ├── 1.properties
│ ├── 2.avi
│ └── 3.diff
├── 2
│ ├── 1.png
│ ├── 2.jpg
│ └── 3.gif
├── 3
│ ├── 1.zip
│ └── 2.mpeg
└── 4
└── 1.mov
Important: keep in mind that this is just a quick and dirty solution and there's no checking for files/directoris already named "1" "2" etc.
How can I recursively delete all files ending in .foo which have a sibling file of the same name but ending in .bar? For example, consider the following directory tree:
.
├── dir
│ ├── dir
│ │ ├── file4.bar
│ │ ├── file4.foo
│ │ └── file5.foo
│ ├── file2.foo
│ ├── file3.bar
│ └── file3.foo
├── file1.bar
└── file1.foo
In this example file.foo, file3.foo, and file4.foo would be deleted since there are sibling file{1,3,4}.bar files. file{2,5}.foo should be left alone leaving this result:
.
├── dir
│ ├── dir
│ │ ├── file4.bar
│ │ └── file5.foo
│ ├── file2.foo
│ ├── file3.bar
└── file1.bar
Remember to first take a backup before you try this find and rm command.
Use this find:
find . -name "*.foo" -execdir bash -c '[[ -f "${1%.*}.bar" ]] && rm "$1"' - '{}' \;
while IFS= read -r FILE; do
rm -f "${FILE%.bar}".foo
done < <(exec find -type f -name '*.bar')
Or
find -type f -name '*.bar' | sed -e 's|.bar$|.foo|' | xargs rm -f
In bash 4.0 and later, and in zsh:
shopt -s globstar # Only needed by bash
for f in **/*.foo; do
[[ -f ${f%.foo}.bar ]] && rm ./"$f"
done
In zsh, you can define a selective pattern that matches files ending in .foo only if there is a corresponding .bar file, so that rm is invoked only once, rather than once per file.
rm ./**/*.foo(e:'[[ -f ${REPLY%.foo}.bar ]]':)