Mac OS X Terminal batch rename...but with folder paths - macos

I tried incorrectly to add my question on to a very similar thread w/ good solutions here:
mac os x terminal batch rename
I have essentially the same question, but I'm wanting to do this and change the folder path when renaming. Here is what I asked:
Would any of these solutions work to change underscores to a folder path? For example, I have mbox files on one level that need to be nested, such as:
TopLevel_NextLevel_mbox
TopLevel_NextLevel_FinalLevel_mbox
I'd like to automatically put these in a hierarchy like so:
TopLevel/NextLevel/mbox
TopLevel/NextLevel/FinalLevel/mbox
Can this be done? When I try simple replacement with "/", I get this:
fred$ for f in *_mbox; do mv "$f" "${f/_//}"; done
mv: rename TopLevel_NextLevel_mbox to TopLevel/NextLevel_mbox: No such file or directory
Looks like it just tries to sub in the "/", but then gets confused because there is no current folder TopLevel w/ NextLevel_mbox inside it...
Thanks,
Fred

The basics of making this work starts with the process of creating an array from the current directories that contain *mbox. Each array key then contains the resulting delimited word found between the underscores:
TopLevel_NextLevel_mbox
Is transformed into an array like this:
( TopLevel, NextLevel, mbox )
From there we create the first directory TopLevel then perform a cd followed by mkdir on the next key — repeating the process until there are no more keys. By doing this each array key creates a new nested directory (as a bonus it also copies any data from the original directory into the new one whilst keeping it's structure).
Create Nested Folders from Original
#!/bin/bash
DIR=$PWD
for f in *mbox
do cd $DIR
if [[ -d $f ]]; then
ARR=(${f//_/ }); n=0
for i in "${ARR[#]}"
do echo $n
if [[ $n -eq 0 ]]; then
mkdir -p $i && cp -R $f/* $i && cd $_
else
mkdir -p $i && cd $_
fi
let n++
done
fi
done
Pseudo One-liner
DIR=$PWD; for f in *mbox; do cd $DIR; if [[ -d $f ]]; then ARR=(${f//_/ }); n=0; for i in "${ARR[#]}"; do if [[ $n -eq 0 ]]; then mkdir -p $i && cp -R $f/* $i && cd $_; else mkdir -p $i && cd $_; fi; let n++; done; fi; done
This is the same exact script as the one above it, however, it's formatted to be one line. * The script leaves the original directories intact (I'll leave the exercise of removing them up to the OP).

Related

Use of CD in Bash For Loop - only getting relative path

I have a small script that I use to organizes files in directories, and now I am attempting to run it on a folder or directories. MasterDir/dir1, MasterDir/dir2, etc.
Running the following code on MasterDir results in an error as the $dir is only a relative path, but I can't figure out how to get the full path of $dir in this case
for dir in */; do
echo $dir
cd $dir
cwd="$PWD"
mkdir -p "VSI"
mv -v *.vsi "$cwd/VSI"
mv -v _*_ "$cwd/VSI"
done
I'd suggest using parentheses to run the loop body in a subshell:
for dir in */; do
(
echo $dir
cd $dir
cwd="$PWD"
mkdir -p "VSI"
mv -v *.vsi "$cwd/VSI"
mv -v _*_ "$cwd/VSI"
)
done
Since that's running in a subshell, the cd command won't affect the parent shell executing the script.
The problem you are having is that after you cd "$dir" the first time, you are one directory below where you generated your list of directories with for dir in */. So the next time you call cd "$dir" it fails because you are still in the first subdirectory you cd'ed into and the next "$dir" in your list is one level above.
There are several ways to handle this. One simple one is to use pushd to change to the directory instead of cd, so you can popd and return to your original directory. (though in this case you could simply add cd .. to change back to the parent directory since you are only one-level deep)
Using pushd/popd you could do:
for dir in */; do
echo $dir
pushd "$dir" &>/dev/null || {
printf "error: failed to change to %s\n" "$dir" >&2
continue
}
cwd="$PWD"
mkdir -p "VSI" || {
printf "error: failed to create %s\n" "$cwd/VSI" >&2
continue
}
mv -v *.vsi "$cwd/VSI"
mv -v _*_ "$cwd/VSI"
popd &>/dev/null || {
printf "error: failed to return to parent dir\n" >&2
break
}
done
(note: the || tests validate the return of pushd, mkdir, popd causing the loop to either continue to the next dir or break the loop if you can't return to the original directory. Also note the &>/dev/null just suppresses the normal output of pushd/popd, and redirection of output to >&2 sends the output to stderr instead of stdout)
As mentioned in the comment, you can always use readlink -f "$dir" to generate the absolute path to "$dir" -- though it's not really needed here.
This is one of the reasons I tend to avoid using cd in shell scripts -- all relative file paths change meaning when you cd, and if you aren't very careful about that you can get into trouble. The other is that cd can fail (e.g. because of a permissions error), in which case you'd better have an error check & handler in place, or something even weirder will happen.
IMO it's much safer to just use explicit paths to refer to files in other directories. That is, instead of cd somedir; mkdir -p "VSI", use `mkdir -p "somedir/VSI". Here's a rewrite of your loop using this approach:
for dir in */; do
echo $dir
mkdir -p "${dir}/VSI"
mv -v "${dir}"/*.vsi "${dir}/VSI"
mv -v "${dir}"/_*_ "${dir}/VSI"
done
Note: the values of $dir will end with a slash, so using e.g. ${dir}/VSI will give results like "somedir//VSI". The extra slash is redundant, but shouldn't cause trouble; I prefer to use it for clarity. If it's annoying (e.g. in the output of mv -v), you can leave off the extra slash.

bash script to pattern match and delete files

Problem: In a directory, I have files of the following form:
<account-number>-<invoice-number>, an example being:
123456-3456789
123456-6789023
123456-2568907
...
456789-2347890
456789-2344357
etc.
What I want to do is, if there are more than 1 invoices for the same account, delete all except the latest. If there's only one, leave it alone.
Thanks for any pointers.
You can use this awk based script:
mkdir _tmp
ls -rt *-*|awk -F'-' '{a[$1]=$0} END{for (i in a) system("mv " a[i] " _tmp/")}'
Once you're satisfied with the files in ./_tmp/ remove all files from current directory and move files over.
Here is a pure bash solution (replace echo with rm when you validate it)
for file1 in *-*
do
IFS=- arr=($file1)
for file2 in "${arr[0]}"*
do
[ "$file1" -nt "$file2" ] && echo "$file2"
done
done
A nice one in Bash:
(pseudo "in place" processing)
#!/bin/bash -e
ADIR="/path/to/account/directory"
TMP="$ADIR.tmp"
mkdir "$TMP" && rmdir "$TMP" && mv "$ADIR" "$TMP" && mkdir "$ADIR"
while IFS=- read ACCNT INVOICE < <( ls -t1 "$TMP" )
do
mv "$TMP/$ACCNT-$INVOICE" "$ADIR/$ACCNT-$INVOICE" && rm "$TMP/$ACCNT"*
done
rmdir "$ADIR.tmp"
what it does:
1 first move the a(ccounts) directory to a temporary directory. (is atomic)
2 in a loop: list newest invoice, move it to the new directory, delete invoices with same account.
3 remove temporary directory
PROs:
solid, safe, short, reasonably fast and halts on serious errors
CONs:
Very definitive, be sure to have always a backup
Comment:
You may have noticed mkdir "$TMP" && rmdir "$TMP"
This is on purpose: rmdir gives the same returnvalue for "dir not exist" as "dir not empty"
so instead of checking which of the two it is
[ -d $DIRNAME ] && { rmdir $DIRNAME || exit }
I used the above construction.
Also the ls -t1 "$TMP may be at a strange place at first sight
But it is OK, every iteration it will be executed again (but only the first line is read)

Bash: Creating subdirectories reading from a file

I have a file that contains some keywords and I intend to create subdirectories into the same directory of the same keyword using a bash script. Here is the code I am using but it doesn't seem to be working.
I don't know where I have gone wrong. Help me out
for i in `cat file.txt`
do
# if [[ ! -e $path/$i ]]; then
echo "creating" $i "directory"
mkdir $path/$i
# fi
grep $i file >> $path/$i/output.txt
done
echo "created the files in "$path/$TEMP/output.txt
You've gone wrong here, and you've gone wrong here.
while read i
do
echo "Creating $i directory"
mkdir "$path/$i"
grep "$i" file >> "$path/$i"/output.txt
done < file.txt
echo "created the files in $path/$TEMP/output.txt"
78mkdir will refuse to create a directory, if parts of it do not exist.
e.g. if there is no /foo/bar directory, then mkdir /foo/bar/baz will fail.
you can relax this a bit by using the -p flag, which will create parent directories if necessary (in the example, it might create /foo and /foo/bar).
you should also use quotes, in case your paths contain blanks.
mkdir -p "${path}/${i}"
finally, make sure that you are actually allowed to create directories in $path

Passing Variables through Nested Shell Scripts

I have a simple loop in my Shell Script and want to be able to run another loop for each file in the directory...right now I am just asking for it to echo each file but I do want to do more with each file, but want to figure this out first...
for dir in "$#/"*
do
folder=$(basename "$dir")
path="/Users/wme/Desktop/test3/"
if [ ! -d "$path$folder" ]; then
mkdir "$path$folder"
mkdir "$path$folder/mp4"
mkdir "$path$folder/mov"
mkdir "$path$folder/ogg"
mkdir "$path$folder/webm"
mkdir "$path$folder/img"
fi
for file in "$dir/"*
echo $file
done
done
So essentially it is finding all the directories creating a folder structure for those folders, and now I need to go and process the movies in each of those folders...but I get an error when I try to do the second nested loop, what am I doing wrong?
Also it should be noted I am running this in Automator so it doesn't really give me an error, just says action failed
You will also have an issue with your first for loop. "$#/"* will expand to $1 $2 ... $n/*, not $1/* $2/* ... $n/*. You'll need
path="/Users/wme/Desktop/test3"
for dir in "$#"; do
for subdir in "$dir/"*; do
folder=$(basename "$subdir")
if [ ! -d "$path/$folder" ]; then
for ftype in mp4 mov ogg webm img; do
mkdir -p "$path/$folder/$ftype"
done
fi
for file in "$subdir/"*
echo $file
done
done

check if the pwd is / in bash

I am trying to write a bash script which will "cd .." until either a directory is found containing another specific directory, or the "/" directory is reached.
What I'm trying to achieve is something like:
while test -d REQUIRED_DIR || [[ "$PWD" != "/" ]];do cd ..; done
The goal is to find the REQUIRED_DIR or stop navigating when the root directory is reached.
Below is the bash code which does not work.
while if [ "$PWD"=="/" ];then echo $PWD;false;else true;fi
do
cd ..
#echo `pwd`
done
The if condition always passes even when not inside root directory.
UPDATE:- the "if" works as expected when i leave space b/w the "==" sign like this
[ "$PWD" == "/" ].
while if is a new one on me, I'm not sure entirely what you're trying to achieve by combining them.
I think you may want to just try while on it's own:
while [[ "$PWD" != "/" ]] ; do
cd ..
done
or you could just try the much simpler:
cd /
If, as indicated in your comment, you need to do it piecemeal so that you either end up in the first directory containing a $REQUIRED_DIR or / if you don't find one, you can simply do something like:
while [[ "$PWD" != "/" ]] ; do
if [[ -d "$REQUIRED_DIR" ]] ; then
break
fi
cd ..
done

Resources