I have an old .bat file that I need to run on my QNAP. I need to recursively create folders for each file (based on file name) then move the file to that folder.
Here's the old .bat:
#echo off
for %%a in (*.*) do (md "%%~na" 2>nul
move "%%a" "%%~na"
)
If I understand the .bat syntax correctly, this should be basically equivalent:
#!/bin/sh
for a in *
do
[ ! -f "$a" ] && continue # skip anything that is not a regular file
dirname="${a%.*}" # trim off the last suffix
mkdir -p "$dirname" && mv -i "$a" "$dirname/"
done
Like the original, this script will process the files within the current directory only. If you need to process the current directory and all its pre-existing sub-directories (that's how I would understand "recursively") it gets a bit more complex, as the script would have to proceed in a depth-first manner to avoid trying to push files into infinitely deep sub-sub-sub... directories.
In that situation, a different solution would need to be developed, most likely using the find command to gather the filenames. The find command is recursive by default: it will automatically process the specified directory and all its sub-directories, unless you specifically tell it not to. So here's a recursive version that can handle an arbitrary number of files in any directory:
#!/bin/sh
# FIXME: needs more protection against unusual filename characters
find . -type f -depth | while read a
do
dirname="${a%.*}" # trim off the last suffix
mkdir -p "$dirname" && mv -i "$a" "$dirname/"
done
It has a slight problem, though: if a filename includes a line-feed character, it might not process that filename correctly. But if your files only have well-behaved characters in their names, this should be enough.
Related
I have a script that, when I right click on a folder, combines all pngs/jpgs/tifs inside the folder into a PDF and renames the PDF to the name of the folder it resides in.
cd %~dpnx1
for %%a in (.) do set currentfolder=%%~na
start cmd /k magick "*.{png,jpg,tif}" "%currentfolder%.pdf"
However, I have quite a lot of folders and currently have to do this one by one.
How can I create a function where I can right click on a folder, which searches subfolders and combines the jpgs to PDF?
So in the example below, Im wanting to create 3 PDFS (Folder A, Folder B and Folder C) by right clicking and running batch on the parent folder.
Example:
Parent folder (one that I would right click and run script from)
|- Folder A
||- test1.jpg
||- test2.jpg
||- test3.jpg
|- Folder B
||- example1.jpg
|| - example2.jpg
|- Folder C
|| Folder D
|||- temp.jpg
|||- temp2.jpg
I have also recently moved to Mac so I'm looking to use zsh. I've had some help to attempt to use the following myself but no luck:
#!/bin/bash
# Set the output directory
output_dir='./pdfs/'
# Make the output directory if it doesn't exist
mkdir -p "$output_dir"
# Check if an input directory was provided as a command-line argument
if [ $# -eq 0 ]
then
# Use the current directory as the input directory if none was provided
input_dir='./'
else
# Use the first command-line argument as the input directory
input_dir="$1"
fi
# Find all the directories in the input directory
find "$input_dir" -type d | while read dir; do
# Extract the base directory name
dirname=$(basename "$dir")
# Create a PDF file with the same name as the base directory name
output_file="$output_dir/$dirname.pdf"
# Find all the JPEG files in the current directory
find "$dir" -type f -name '*.jpg' | while read file; do
# Convert the JPEG file to PDF and append it to the output file
convert "$file" "$file.pdf"
done
# Concatenate all the PDF files in the current directory into a single PDF
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile="$output_file" "$dir"/*.pdf
# Remove the temporary PDF files
rm "$dir"/*.pdf
done
Hope you can help. Thank you.
There are several aspects to this question, and judging by your attempted solution, they will all be non-trivial for you. It is more than a normal question so I'll just give you an outline so you can tackle it in chunks. You'll need to:
install homebrew
install ImageMagick
use Automator to make a workflow for right-click
learn some bash scripting to recurse through directories
learn some ImageMagick to make PDFs
Install homebrew
Go to here and follow instructions to install homebrew. I am not repeating the instructions here as they may change.
You'll likely need to install Xcode command-line tools with:
xcode-select --install
You'll need to set your PATH properly afterwards. Don't omit this step.
Install ImageMagick
You'll need to do:
brew install imagemagick
Setup workflow with Automator for right-click
Next you need to make a script that will be executed when you right-click on a directory. It will look like this when we have done it. I right-clicked on the Junk directory on my desktop and went down to Quick Actions and across to makePDFs.
So, in order to do that you need to start the Automator by clicking ⌘ SPACE and typing Automator and hitting ENTER when it guesses.
Then select New Document and Quick Action. Now navigate the orange areas in the diagram till you find Run Shell Script then drag Run Shell Script over to the right side and drop it in the blue zone. Go on the Edit menu and click ⌘ and then Save As and enter makePDFs in the box. This is the name that will appear in future on your right-click menu.
Now set the options in the green box like I have done.
Now replace all the code in the blue box with the code copied from below:
#!/bin/bash
################################################################################
# Recurse into all subdirectories specified in parameter and make PDF in each
# directory of all images found in there.
################################################################################
# Add ImageMagick from homebrew to PATH
PATH=$PATH:/opt/homebrew/bin
# Check we got a directory as parameter
if [ $# -ne 1 ] ; then
>&2 echo "Usage: $0 DIRECTORY"
exit 1
fi
# Find and process all subdirectories
shopt -s nullglob
while read -rd $'\0' dir; do
# Start a subshell so we don't have to cd back somewhere
(
cd "$dir" || exit 1
# Make list of all images in directory
declare -a images
for image in *.jpg *.png *.tif ; do
images+=("$image")
done
numImages=${#images[#]}
if [ $numImages -gt 0 ] ; then
pdfname=${PWD##*/}
magick "${images[#]}" "${pdfname}.pdf"
fi
)
done < <(find "$1" -type d -print0)
Finally, set the options like I did in the cyan coloured box.
Now save the whole workflow again and everything should work nicely.
For bash, try this (tested on Linux):
for d in */; do convert "$d"/*.{png,jpg,tif} "$d/${d%/}.pdf" ; done
In slo-mo:
for d in */: loop on all directories in current directory (the / restrict matches to directories)). Variable $d contains the directory name, and name has a final /.
"$d"/*.{png,jpg,tif}: all files with png, jpg or tif extension in the directory "$d"
"$d/${d%/}.pdf": the directory name, a slash, the directory name with the ending slash removed, and .pdf`
If you look carefully, the explicit /s in the code aren't necessary since there is already one at the end of $d, but leaving them in makes the code a bit more readable and the multiple '//' are coalesced into a single one.
This code may however complain that there are no png/jpg/tif. A slightly different form makes it behave more nicely:
shopt -s extglob # this is possibly already set by default
for d in */; do convert "$d"/*.#(png|jpg|tif) "$d/${d%/}".pdf ; done
for zsh this could be (untested!):
# shopt -s extglob # no shopt necessary
for d in */; do convert "$d"/*.png(N) "$d"/*.jpg(N) "$d"/*.tif(N) "$d/${d%/}".pdf ; done
with the caveat that if no pattern matches, the command will just be convert whatever_output.pdf and you will get the built-in help.
The difference is that *.{png,jpg,tif} is expanded to *.png *.jpg *.tif before any pattern matching is done, so this represents three file patterns and the shell tries to match each pattern in turn (and leaves the literal pattern in case there is no match), while *.#(png|jpg|tif) is a single file pattern that matches any of the three extensions. This can also make a difference for you because the files do not appear in the same order, *.{png,jpg,tif} lists all the PNG, then the JPG,then the TIF, while *.#(png|jpg|tif) has them all sorted in alphabetical order without regard for the extension.
I'm looking to find a way to constantly scan a folder tree for new subfolders containing MKV/MP4 files. If that file contains a keyword and ends in MP4 or MKV, it'll be moved to a defined location matching that keyword. As a bonus, it would delete the folder and all it's leftover contents where the file resided previosly. The idea would be to have this run in the background and sort everything where it belongs and clean up after itself if possible.
example:
Media\anime\Timmy\Timmy_S1E1\Timmy_S1E1_720p.mkv #Found Keyword Timmy, allowed filetype
Move to destination:
Media\series\Timmy\
Delete subfolder:
Media\anime\Timmy\Timmy_S1E1\
I would either do separate scripts for each keyword, or, if possible, have the script match each keyword with a destination
#!/bin/bash
#!/bin/sh
#!/etc/shells/bin/bash
while true
do
shopt -s globstar
start_dir="//srv/MEDIA2/shows"
for name in "$start_dir"/**/*.*; do
# search the directory recursively
done
sleep 300
done
This could be done by:
creating a script that does what you want to do, once.
run the script from cron, at a certain interval. Say a couple minutes, or a couple hours, depends on the volume of files you receive.
no need for a continually running daemon.
Ex:
#!/bin/bash
start_dir="/start/directory"
if [[ ! -d "$start_dir" ]]
then
echo "ERROR: start_dir ($start_dir) not found."
exit 1
fi
target_dir="/target/directory"
if [[ ! -d "$target_dir" ]]
then
echo "ERROR: target_dir ($target_dir) not found."
exit 1
fi
# Move all MP4 and MKV files to the target directory
find "$start_dir" -type f \( -name "*keyword*.MP4" -o -name "*keyword*.MKV" \) -print0 | while read -r -d $'\0' file
do
# add any processing here...
filename=$(basename "$file")
echo "Moving $filename to $target_dir..."
mv "$file" "$target_dir/$filename"
done
# That being done, all that is left in start_dir can be deleted
find "$start_dir" -type d ! -path "$start_dir" -exec /bin/rm -fr {} \;
Details:
scanning for files is most efficient with the find command
the -print0 with read ... method is to ensure all valid filenames are processed, even if they include spaces or other "weird" characters.
the result of the above code is that each file that matches your keyword, with extensions MP4 or MKV will be processed once.
you can then use "$file" to access the file being processed in the current loop.
make sure you ALWAYS double quote $file, otherwise any weird filename will brake your code. Well you should always double quote your variables anyway.
more complex logic can be added for your specific needs. Ex. create the target directory if it does not exist. Create a different target directory depending on your keyword. etc.
to delete all sub-directories under $start_dir, I use find. Again this will process weird directory names.
One point, some will argue that it could all be done in 1 find command with -exec option. True, but IMHO the version with the while loop is easier to code, understand, debug, learn.
And this construct is good to have in your bash toolbox.
When you create a script, only one #! line is needed.
And I fixed the indentation in your question, much easier to read your code properly indented and formatted (see the edit help in the question editor).
Last point to discuss, lets say you have a LARGE number of directories and files to process, and it is possible that new files are added while the script is running. Ex. you are moving many MP4 files, and while it is doing it, new files are deposited in the directories. Then when you do the deletion you could potentially loose files.
If such a case is possible, you could add a check for new files just before you do the /bin/rm, it would help. To be absolutely certain, you could setup a script that processes 1 file, and have it triggered by inotify. But that is another ball game, more complicated and out of scope for this answer.
I am having difficulty moving files and prepending to the files with Bash.
#!/bin/bash
CAT="WFS_CAT"
for FILENAME in /foo/bar/20*
do
mv "${FILENAME##*/}" "${CAT}.${FILENAME##*/}"
done;
The command errors out. It tries to move the full directory name and prepend to that instead of the individual files.
In this case, it should be much simpler to change the directory to the one your files are located in, as your move command only renames files in this directory. Do you mind trying this?
pushd /foo/bar
for FILENAME in 20*; do
echo mv "${FILENAME}" "${CAT}.${FILENAME}"
done;
popd
I have a few files with the format ReportsBackup-20140309-04-00 and I would like to send the files with same pattern to the files as the example to the 201403 file.
I can already create the files based on the filename; I would just like to move the files based on the name to their correct folder.
I use this to create the directories
old="directory where are the files" &&
year_month=`ls ${old} | cut -c 15-20`&&
for i in ${year_month}; do
if [ ! -d ${old}/$i ]
then
mkdir ${old}/$i
fi
done
you can use find
find /path/to/files -name "*201403*" -exec mv {} /path/to/destination/ \;
Here’s how I’d do it. It’s a little verbose, but hopefully it’s clear what the program is doing:
#!/bin/bash
SRCDIR=~/tmp
DSTDIR=~/backups
for bkfile in $SRCDIR/ReportsBackup*; do
# Get just the filename, and read the year/month variable
filename=$(basename $bkfile)
yearmonth=${filename:14:6}
# Create the folder for storing this year/month combination. The '-p' flag
# means that:
# 1) We create $DSTDIR if it doesn't already exist (this flag actually
# creates all intermediate directories).
# 2) If the folder already exists, continue silently.
mkdir -p $DSTDIR/$yearmonth
# Then we move the report backup to the directory. The '.' at the end of the
# mv command means that we keep the original filename
mv $bkfile $DSTDIR/$yearmonth/.
done
A few changes I’ve made to your original script:
I’m not trying to parse the output of ls. This is generally not a good idea. Parsing ls will make it difficult to get the individual files, which you need for copying them to their new directory.
I’ve simplified your if ... mkdir line: the -p flag is useful for “create this folder if it doesn’t exist, or carry on”.
I’ve slightly changed the slicing command which gets the year/month string from the filename.
I have this find
find "$source_folder" -name "IMG_[0-9][0-9][0-9][0-9].JPG" -exec cp {} $destination_folder \;
i want only the IMG_[0-9][0-9][0-9][0-9].JPG, in the source folder there are diferent files with same name,and same files with same name, how can i copy everything and rename all the same name files with extra .JPG without deleting any unique files?
PS: noob, please could you explain so i can try and learn
I'll assume that your file names don't contain any whitespace. It makes things easier.
You can pipe the output of the find command to a loop where you can run some tests whether or not you want to copy the file.
I have to determine the name of the file and where it is copied to. In order to do that, I have to strip off the $source_folder from the name of the file I find, and prepend the name of the $dest_folder. This is where I want to actually copy the file.
Your directions are a bit confusing. I am assuming you're doing the copy if the $dest_file doesn't exist or it is different from the source. Once I determine that this is the file you want me to copy, I have to make sure the destination directory exists, and if it doesn't I create it. Now, I can do my copy.
I have two echo statements in here. This way, you can do a dry run of this script to make sure it's doing what you want it to do. If it looks good, you can remove the echo commands from the two lines and rerun the script.
find "$source_folder" -name "IMG_[0-9][0-9][0-9][0-9].JPG" | while read file_name
do
rootname=${file_name#$source_folder/} # Removes the source folder from the name
dest_name="${dest_folder}/$rootname"
if [ ! -e "$dest_name" ] || [ ! diff "$file_name" "$dest_name" > /dev/null 2>&1 ]
then
$dest_folder=$(basename $dest_name)
[ ! -d "$dest_folder" ] && echo mkdir -p "$dest_folder" #Remove echo if it works
echo cp "$file_name" "$dest_name" #Remove 'echo' if it works
fi
done