rename files keeping basename adding extra word and changing extension - bash

I have a script to encode video files. When I run this script, I would like to keep the basename of the file, add a 'modified' string to the filename and also changing the extension.
The following for loop does that with the exception of changing the extension:
for file in *.mkv;
do
encode $file "${file%%.*}_modified.${i#*.}";
done
I'd like that my_file.mkv will become my_file_modified.mp4. The previous loop just converts my_file.mkv into my_file_modified.mkv
How to change also the extension from .mkv to .mp4?
Thanks in advance.

I'm not totally sure if I got your question right, and probably not; but if I did, you should just do this:
for file in *.mkv;
do
encode $file "${file%.*}_modified.mp4";
done
The ${i#*.} part in your previous command actually took the original extension from the file name; you can just omit it and set your own extension instead.
Also, as #M.NejatAydin pointed out in the comment, you should use ${file%.*} instead of ${file%%.*}, to keep the entire original filename if it has a dot inside it.
For example:
$ file="test.file.mkv"
$ echo "${file%%.*}_modified.mp4"
test_modified.mp4 # This is probably NOT what you want
$ echo "${file%.*}_modified.mp4"
test.file_modified.mp4 # This is probably what you want

Related

BASH Shell Find Multiple Files with Wildcard and Perform Loop with Action

I have a script that I call with an application, I can't run it from command line. I derive the directory where the script is called and in the next variable go up 1 level where my files are stored. From there I have 3 variables with the full path and file names (with wildcard), which I will refer to as "masks".
I need to find and "do something with" (copy/write their names to a new file, whatever else) to each of these masks. The do something part isn't my obstacle as I've done this fine when I'm working with a single mask, but I would like to do it cleanly in a single loop instead of duplicating loop and just referencing each mask separately if possible.
Assume in my $FILESFOLDER directory below that I have 2 existing files, aaa0.csv & bbb0.csv, but no file matching the ccc*.csv mask.
#!/bin/bash
SCRIPTFOLDER=${0%/*}
FILESFOLDER="$(dirname "$SCRIPTFOLDER")"
ARCHIVEFOLDER="$FILESFOLDER"/archive
LOGFILE="$SCRIPTFOLDER"/log.txt
FILES1="$FILESFOLDER"/"aaa*.csv"
FILES2="$FILESFOLDER"/"bbb*.csv"
FILES3="$FILESFOLDER"/"ccc*.csv"
ALLFILES="$FILES1
$FILES2
$FILES3"
#here as an example I would like to do a loop through $ALLFILES and copy anything that matches to $ARCHIVEFOLDER.
for f in $ALLFILES; do
cp -v "$f" "$ARCHIVEFOLDER" > "$LOGFILE"
done
echo "$ALLFILES" >> "$LOGFILE"
The thing that really spins my head is when I run something like this (I haven't done it with the copy command in place) that log file at the end shows:
filesfolder/aaa0.csv filesfolder/bbb0.csv filesfolder/ccc*.csv
Where I would expect echoing $ALLFILES just to show me the masks
filesfolder/aaa*.csv filesfolder/bbb*.csv filesfolder/ccc*.csv
In my "do something" area, I need to be able to use whatever method to find the files by their full path/name with the wildcard if at all possible. Sometimes my network is down for maintenance and I don't want to risk failing a change directory. I rarely work in linux (primarily SQL background) so feel free to poke holes in everything I've done wrong. Thanks in advance!
Here's a light refactoring with significantly fewer distracting variables.
#!/bin/bash
script=${0%/*}
folder="$(dirname "$script")"
archive="$folder"/archive
log="$folder"/log.txt # you would certainly want this in the folder, not $script/log.txt
shopt -s nullglob
all=()
for prefix in aaa bbb ccc; do
cp -v "$folder/$prefix"*.csv "$archive" >>"$log" # append, don't overwrite
all+=("$folder/$prefix"*.csv)
done
echo "${all[#]}" >> "$log"
The change in the loop to append the output or cp -v instead of overwrite is a bug fix; otherwise the log would only contain the output from the last loop iteration.
I would probably prefer to have the files echoed from inside the loop as well, one per line, instead of collect them all on one humongous line. Then you can remove the array all and instead simply
printf '%s\n' "$folder/$prefix"*.csv >>"$log"
shopt -s nullglob is a Bash extension (so won't work with sh) which says to discard any wildcard which doesn't match any files (the default behavior is to leave globs unexpanded if they don't match anything). If you want a different solution, perhaps see Test whether a glob has any matches in Bash
You should use lower case for your private variables so I changed that, too. Notice also how the script variable doesn't actually contain a folder name (or "directory" as we adults prefer to call it); fixing that uncovered a bug in your attempt.
If your wildcards are more complex, you might want to create an array for each pattern.
tmpspaces=(/tmp/*\ *)
homequest=($HOME/*\?*)
for file in "${tmpspaces[#]}" "${homequest[#]}"; do
: stuff with "$file", with proper quoting
done
The only robust way to handle file names which could contain shell metacharacters is to use an array variable; using string variables for file names is notoriously brittle.
Perhaps see also https://mywiki.wooledge.org/BashFAQ/020

Iterating through pairs of files with glob

I'm having a difficult time trying to iterate through a long set of files that I need to pair up to run through some process. I'd like to generate a bit of a batch file, pairing each set of matching files one per line. I've done this kind of thing before when it's a simple replacement (e.g. file1 = something.txt, file2 = something.csv). But in this case, the end of the file string is a random UUID, and I can't figure out how to get bash to properly expand the glob the second file.
Given a directory of files like this:
banana_pre-proc_b101a65a-31c7-5e4f-b433-bac4fb1efc1f.txt
banana_proc_a75b3a3e-7140-1cb6-2ad1-c10f7db6743f.txt
cherry_pre-proc_f5d0716f-c205-b0b4-5c63-d33755767de4.txt
cherry_proc_025ff6d5-534d-0020-5446-5da3ed04adc6.txt
kiwi_pre-proc_26075f3b-e3a2-fc1a-a741-615cacfc1a7e.txt
kiwi_proc_be1760f6-413d-edc0-1efc-a134b1b6bfbb.txt
peach_pre-proc_ecafbb30-3df0-6014-61ee-11d1d5745b53.txt
peach_proc_bb3ea3fc-671e-e024-6e61-06a2bc147363.txt
pear_pre-proc_c2db376f-f351-7141-114e-a2ebc3cfc410.txt
pear_proc_ccb2f16a-27cd-c70d-7aac-ce72c3af6575.txt
How can I get a file that looks like:
banana_pre-proc_b101a65a-31c7-5e4f-b433-bac4fb1efc1f.txt banana_proc_a75b3a3e-7140-1cb6-2ad1-c10f7db6743f.txt
cherry_pre-proc_f5d0716f-c205-b0b4-5c63-d33755767de4.txt cherry_proc_025ff6d5-534d-0020-5446-5da3ed04adc6.txt
kiwi_pre-proc_26075f3b-e3a2-fc1a-a741-615cacfc1a7e.txt kiwi_proc_be1760f6-413d-edc0-1efc-a134b1b6bfbb.txt
peach_pre-proc_ecafbb30-3df0-6014-61ee-11d1d5745b53.txt peach_proc_bb3ea3fc-671e-e024-6e61-06a2bc147363.txt
pear_pre-proc_c2db376f-f351-7141-114e-a2ebc3cfc410.txt pear_proc_ccb2f16a-27cd-c70d-7aac-ce72c3af6575.txt
I thought I could do something like
for f in *pre-proc_*txt; do echo "$f" "${f/-pre-proc_/-proc_}"; done
But that doesn't deal with the UUID at the end of the file. I've tried a few other iterations of this strategy too, but none get any closer. What is the trick to doing this? Obviously for a few files like this, I can just manually do it. But, the actual set of files I need to process is quite long and apart from just pulling them all into a text doc and then using some Vim macro or something, I'm a bit baffled as to how to get Bash to expand the glob like I'm intending.
This seems to work:
for preproc in *_pre-proc*; do
base=${preproc%_pre-proc*}
proc=${base}_proc*
echo $preproc $proc
done
We get a base name by stripping of the _pre_proc<uuid> part, and
then use the base name to find the matching _proc file.
This I think should be sufficient:
printf "%s %s\n" *[-_]proc_*.txt
Glob expansions are sorted and the pairs of files share the same prefix.

OSX / MacOs batch rename hexadecimal filenames to decimal filenames

I want to rename filenames with a hexadecimal part in the name to decimal. For example: MOV12B.MOD, MOV12C.MOD etc. To MOV299.mod, MOV300.MOD.
Can this be done in terminal?
It is possible to rename the extension using:
find . -name "*.MOD" -exec rename 's/\.MOD$/.MPG/' '{}' \;
But how can I rename the files to decimal?
Sure, you can do it with rename, also known as Perl rename and prename which is most simply installed on macOS with homebrew using:
brew install rename
Then the command is:
rename --dry-run 's/[0-9A-F]+/hex($&)/e' *MOD
Sample Output
'MOV10.MOD' would be renamed to 'MOV16.MOD'
'MOV12B.MOD' would be renamed to 'MOV299.MOD'
'MOV12C.MOD' would be renamed to 'MOV300.MOD'
'MOVBEEF.MOD' would be renamed to 'MOV48879.MOD'
If you like what it does, remove the --dry-run part and do it for real.
I would recommend you make a backup before trying this anyway, because if your films are actually named "Film 23.MOD" rather than "MOV12B.MOD" you will get:
'Film 23.MOD' would be renamed to '15ilm 23.MOD'
If you want to put the date in too, you can do:
rename --dry-run 's/[0-9A-F]+/hex($&)/e; s|.MOD| 17/01/2018.MOD|' *MOD
Sample Output
'MOV12A.MOD' would be renamed to 'MOV298 17/01/2018.MOD'
Why couldn't you find it in the man-page? Well, there is a line in there that casually says you can pass a line of Perl code to modify the name. That means that the entire Perl language is available to you - so you could write several pages of code that access a database, run something on a remote machine, or fetch a URL in order to rename your file.
The only tricky thing in my code is the e lurking at the end:
s/search/replace/e
The e means that the second half of the search/replace is actually executed so it is not a straight textual replacement, it is a new program that gets the search string from the left-hand side in $& and can do maths or lookups on it.
I have done some other answers that involve similar techniques...
here,
here,
here.
If you want to put the modification time of the file into its name as well, you need to do a little more work. First, stat() the file before changing its name ;-) Remember you receive the original filename in $_. Then do the the hex to decimal thing, then add in the mtime. Remember Perl uses a dot to concatenate strings together.
So, the command is going to look like this:
rename --dry-run 'my $mtime=(stat($_))[9]; s/[0-9A-F]+/hex($&) . " " . $mtime/e;' *MOD
Sample Output
'MOV12A.MOD' would be renamed to 'MOV298 1516229449.MOD'
If all the substitution and evaluation gets too much, you can always do all your calculations and assign the result to Perl's $_ variable through which you receive the into filename and in which you pass the desired name back to rename. So, for an example:
rename --dry-run 'my $prefix="PREFIX "; my $middle=$_; my $suffix=" SUFFIX"; $_=$prefix . $middle . $suffix;' *MOD
'MOV12A.MOD' would be renamed to 'PREFIX MOV12A.MOD SUFFIX'
Only a real programmer would store his movies with hex names - kudos to you!

Add part of filename as PDF metadata using bash script and exiftool

I have about 600 books in PDF format where the filename is in the format:
AuthorForename AuthorSurname - Title (Date).pdf
For example:
Foo Z. Bar - Writing Scripts for Idiots (2017)
Bar Foo - Fun with PDFs (2016)
The metadata is unfortunately missing for pretty much all of them so when I import them into Calibre the Author field is blank.
I'm trying to write a script that will take everything that appears before the '-', removes the trailing space, and then adds it as the author in the PDF metadata using exiftool.
So far I have the following:
for i in "*.pdf";
do exiftool -author=$(echo $i | sed 's/-.*//' | sed 's/[ \t]*$//') "$i";
done
When trying to run it, however, the following is returned:
Error: File not found - Z.
Error: File not found - Bar
Error: File not found - *.pdf
0 image files updated
3 files weren't updated due to errors
What about the -author= phrase is breaking here? Please could someone enlighten me?
You don't need to script this. In fact, doing so will be much slower than letting exiftool do it by itself as you would require exiftool to startup once for every file.
Try this
exiftool -ext pdf '-author<${filename;s/\s+-.*//}' /path/to/target/directory
Breakdown:
-ext pdf process only PDF files
-author the tag to copy to
< The copy from another tag option. In this case, the filename will be treated as a pseudo-tag
${filename;s/\s+-.*//} Copying from the filename, but first performing a regex on it. In this case, looking for 1 or more spaces, a dash, and the rest of the name and removing it.
Add -r if you want to recurse into subdirectories. Add -overwrite_original to avoid making backupfiles with _original added to the filename.
The error with your first command was that the value you wanted to assign had spaces in it and needed to be enclosed by quotes.

Way to move files in bash and rename copied file automatically without overwriting an existing file

I'm doing some major restructuring of large numbers of directories with tons of jpgs, some of which my have the same name as files in other directories. I want to move / copy files to alternate directories and have bash automatically rename them if the name matches another file in that directory (renaming IMG_238.jpg to IMG_238_COPY1.jpg, IMG_238_COPY2.jpg, etc), instead of overwriting the existing file.
I've set up a script that takes jpegs and moves them to a new directory based on exif data. The final line of the script that moves one jpg is: mv -n "$JPEGFILE" "$DIRNAME"
I'm using the -n option because I don't want to overwrite files, but now I have to go and manually sort through the ones that didn't get moved / copied. My GUI does this automatically... Is there a relatively simple way to do this in bash?
(In case it matters, I'm using bash 3.2 in Mac OSX Lion).
This ought to do it
# strip path, if any
fname="${JPEGFILE##*/}"
[ -f "$DIRNAME/$fname" ] && {
n=1
while [ -f "$DIRNAME/${fname%.*}_COPY${n}.${fname##*.}" ] ; do
let n+=1
done
mv "$JPEGFILE" "$DIRNAME/${fname%.*}_COPY${n}.${fname##*.}"
} || mv "$JPEGFILE" "$DIRNAME"
EDIT: Improved.
You can try downloading and seeing if Ubuntu/Debian's Perl-based rename works. It has sed-style functionality. Quoth the man page (on my system, but the script should be the same one as linked):
"rename" renames the filenames supplied according to the rule specified
as the first argument. The perlexpr argument is a Perl expression
which is expected to modify the $_ string in Perl for at least some of
the filenames specified. If a given filename is not modified by the
expression, it will not be renamed. If no filenames are given on the
command line, filenames will be read via standard input.
For example, to rename all files matching "*.bak" to strip the
extension, you might say
rename 's/\.bak$//' *.bak
To translate uppercase names to lower, you'd use
rename 'y/A-Z/a-z/' *

Resources