Streamlining conditional statements (if statement) in bash - bash

My question is more so a question about code efficiency and simplicity than it is about simply completing a task. The scenario is as such: I would like to create a bash script that uses a for loop to iterate through /Users when it is in each users home directory I want it to see if two different directories exist in the style of:
for USER in /Users/*; do
if [ -d "$USER/Library/Caches/com.spotify.Client" ]; then
rm -rf "$USER/Library/Caches/com.spotify.Client"
...but I need to check for multiple directories. How do I accomplish this in the most elegant way? I would like to avoid using a series of if statements but don't know the best way to accomplish this.
Finally, I would like to use the find command to find a file, then set the result of the find (i.e. the path to the found file) to a variable and input it into another command. Thank you.

From what I understand of your requirements, I would nest the for loops:
subdirs=(
"Library/Caches/com.spotify.client"
"some/other/subdir/"
)
for homedir in /Users/*; do
for subdir in "${subdirs[#]}"; do
dir="$homedir/$subdir"
if [[ -d "$dir" ]]; then
rm -rf "$dir"
fi
done
done

The following builds on #David Wolever's answer above; these versions are shorter and let the shell do more globbing up front.
Inline, single-loop version without use of intermediate (array) variable:
for dir in /Users/*/'Library/Caches/com.spotify.client/' /Users/*/'some/other/subdir/'; do
[[ -d "$dir" ]] && rm -rf "$dir"
done
Array-variable version:
subdirs=(
'Library/Caches/com.spotify.client/'
'some/other/subdir/'
)
for subdir in "${subdirs[#]}"; do
for dir in /Users/*/"$subdir"; do
[[ -d "$dir" ]] && rm -rf "$dir"
done
done

Related

Make directory based on filenames before fourth underscore

I have these test files:
ABCD1234__12_Ab2_Hh_3P.mp4
ABCD1234__12_Ab2_Lw_3P.wmv
ABCD1234__12_Ab2_SSV.mov
EFGH56789__13_Mn1_SSV.avi
EFGH56789__13_Mn1_Ve1_3P.mp4
EFGH56789__13_Mn1_Ve2_3P.webm
I want to create a context service in Automator that makes directories based on the filename prefixes above like so:
ABCD1234__12_Ab2
EFGH56789__13_Mn1
...and move the files into those two accordingly. The only consistent variables in the names are underscores, so I was thinking I could delineate by those, preferably capturing the name before the fourth one.
I originally started with this very simple script:
for file in "$#"
do
mkdir "${file%.*}" && mv "$file" "${file%.*}"
done
Which makes a folder for every file and moves each file into its own folder.
I tried adding variables, various if/thens, etc. but to no avail (not a programmer by trade).
I also wrote another script to do it in a slightly different way, but with the same results to mess around with:
for folder in "$#"
do
cd "$1"
find . -type f -maxdepth 1 -exec bash -c 'mkdir -p "${0%.*}"' {} \; \
-exec bash -c 'mv "$0" "${0%.*}"' {} \;
done
I feel like there's something obvious I am missing.
Your script is splitting on dot, but you say you want to split on underscore. If the one you want to split on is the last one, the fix is trivial:
for file in "$#"
do
mkdir -p "${file%_*}" && mv "$file" "${file%_*}"
done
To get precisely the fourth, try
for file in "$#"
do
tail=${file#*_*_*_*_}
dir=${file%_"$tail"}
mkdir -p "$dir" && mv "$file" "$dir"
done
The addition of the -p option is a necessary bug fix if you want to use && here; mkdir without this option will fail if the directory already exists.
Perhaps see also the section about parameter expansions in the Bash Reference Manual which explains this syntax and its variations.
You could do something like this:
#!/bin/bash
shopt -s extglob
for file
do
if [[ $file =~ ^(([^_]*_){3}[^_]*) ]]
then
echo "${BASH_REMATCH[0]}"
else
echo "${file%.*}"
fi
done

Comparing relative paths in bash script

I am trying to build a bash script capable of comparing two directories given as arguments $1 and $2, and changing the files' timestamps from the second directory ( if they are not different than a given timestamp $3 ) to be the same as the files with the same name in the first directory. I'm doing okay with that, but I don't see how to access the folders inside the given directories, and compare the files inside those folders.
For example, if I have Directory1 and Directory2 given as arguments:
Directory1 contains:
-text.txt
-folder1/secondfile.txt
-folder2/thirdfile.txt
and Directory2 contains:
-text.txt
-folder1/secondfile.txt
-folder3/thirdfile.txt
so in this case I want my script to modify the files text.txt and secondfile.txt, but not the thirdfile.txt because the relative paths are not the same. How would my script access folders in the directory and how would it compare relative paths? I have managed to do what I wanted with files from the directory, but not folders, and I have no idea how to compare relative paths, even though I searched around I couldn't find it.
So far I've done this script (with help from SO):
#!/bin/bash
cd "$1"
function check {
for i in *; do
if [[-d "$i"]]; then
cd "$i"
check
cd -
fi
if [[-f "$i"]]; then
if [[stat %y "$i" == "$3"]]; then
#if [[path check]];then
#touch -r "$i" "$2/path/$i"
fi
}
and I don't know how to do the [[path check]] which should check if both files have the same relative path (relative to the directories given as arguments).
EDIT:
As the answer suggests, is the following code the right way to do it?
#!/bin/bash
cd "$1"
shopt -s globstar
for i in **; do
if [[stat %y "$i" == "$3"]]; then
if [["$1/$i" == "$2/$i"]];then
touch -r "$i" "$2/$i"
fi
There was an answer here before, which I wanted to reply to, suggesting using shopt -s globstar and ** instead of *.
The additional question was something along the lines of "Would I be able to compare the relative paths?".
Yes, you would. With shopt -s globstar, ** expands to include the relative path to each file. So it would return text.txt folder1/secondfile.txt folder2/thirdfile.txt.
EDIT:
You should not need to cd "$1" either, for "$1" and "$2" would not exist inside dir "$1". Try something along the lines of:
#!/usr/bin/env bash
shopt -s globstar
for i in $(cd "$1"; echo **); do
if [[ $(stat -c %y "$1/$i") == "$3" ]]; then
if [[ -f "$1/$i" ]] && [[ -f "$2/$i" ]]; then
touch -r "$1/$i" "$2/$i"
fi
fi
done

Glob-like brace Wildcards in Bash

Lets say I've a directory /etc/php5/conf.d/, with the following hypothetical files in it:
mysql.ini
mysqli.ini
20-mysql.ini
20-mysqli.ini
20-pdo_mysql.ini
I would like to delete all these files except the last one (pdo), this is what I have at the moment:
for phpIni in mysql mysqli; do
if [[ -f /etc/php5/conf.d/$phpIni.ini ]]; then
rm /etc/php5/conf.d/$phpIni.ini
if [[ -f /etc/php5/conf.d/20-$phpIni.ini ]]; then
rm /etc/php5/conf.d/20-$phpIni.ini
fi
done
It works, but I can't help noticing that the above could be simplified with glob patterns, such as:
if [[ ! -z /etc/php5/conf.d/{,20-}mysql*ini ]]; then
rm /etc/php5/conf.d/{,20-}mysql*ini
fi
There's a problem though, if the any of the expansions doesn't exists, rm will complain about it:
$ if [[ ! -z /etc/php5/conf.d/{,20-}mysql*ini ]]; then rm /etc/php5/conf.d/{,20-}mysql*ini; fi
rm: cannot remove `/etc/php5/conf.d/20-mysql*ini': No such file or directory
How can I make rm only remove existing files? Or at least prevent it from throwing all these errors?
You can use rm -f, it will not complain if files don't exist: as seen in the man page,
"ignore nonexistent files, never prompt".
Adding a -f as an rm parameter will prevent that. On the flipside, though, you need to ensure your glob is solid, lest you delete too much.
Add shopt -s nullglob -- if there are no files matching the pattern, the null string is substituted instead of the pattern string.
To delete all except one, use
shopt -s extglob
rm !(*pdo*).ini

BASH parameters with wildcard

I'm trying to do a bash script that will find & copy similar files to a destination directory.
For example, I'm passing a parameter 12300 to a script and I want to copy all files that start with 12300... to a new directory.
like this:
sh script.sh 12300
and here's the script:
if [ -f /home/user/bashTest/$#*.jpg ]
then
cp /home/user/bashTest/$#*.jpg /home/user/bashTest/final/
fi
This just doesn't work. I have tried all kinds of solutions but nothing has worked.
The question is: How can I use wildcard with parameter?
When you're checking for multiple files with -f or -e it can get nasty. I recommend kenfallon's blog. This is something like what he would recommend:
#! /bin/bash
ls -l /home/user/bashTest/$1*.jpg > /dev/null
if [ "$?" = "0" ]
then
cp /home/user/bashTest/$1*.jpg /home/user/bashTest/final/
fi
Not sure how the $# would play in here, or if it's required.
Enclose the thing that expands to the parameters in {}, i.e. /home/user/bashTest/${#}*.jpg. You should use $1 instead of $# in your case however as you only seem to be able to handle the first argument given to the script. $1 expands to the first argument, $2 to the second etc.
You also need a loop to iterate over all files that this glob expands to, e.g.
for file in /tmp/${#}*.jpg
do
if [ -f $file ]
then
echo $file
fi
done
Here is a solution:
#!/bin/bash
cp /home/user/bashTest/${1}*.jpg /home/user/bashTest/final/
Discussion
In this case, a simple cp command will do
I have tested it with files that have embedded spaces
Write this in script.sh:
cp /home/user/bashTest/$1*.jpg /home/user/bashTest/final/
That's all.
UPD. #macduff solution usefull too.
This will find all of them in your $HOME directory and subdirectories (you may wish to tweak find to follow/not follow symlinks and/or adjust the $HOME base directory where it starts the search)
#!/bin/sh
DEST=/your/dest/folder
for FILE in `find "$HOME" -iname "$1"*`;do
[ -f "$FILE" ] && mv "$FILE" "$DEST/$FILE"
#or ln -s ...if you want to keep it in its original location
done
if you want to do multiple patterns using $#
for PATTERN in $#; do
for FILE in `find "$HOME" -iname "$PATTERN"*`;do
[ -f "$FILE" ] && mv "$FILE" "$DEST/$FILE"
done
done

mkdir -p vs if [[ ! -d dirname ]]

Is there any reason to use
if [[ ! -d dirname ]]; then mkdir dirname; fi
instead of just
mkdir -p dirname
The first syntax depends on the shell you are using, not the second.
Since both fail if dirname exists not as a directory, no, there's no difference.
-d FILE True if file is a directory.
-p no error if existing, make parent directories as needed.
If dirname does not contain any parents then the two commands behave the same. However if dirname contains parents the -d will not create those. And [[ is shell-dependent.
These two ksh commands are functionally the same since both will create a directory called dirname.
mkdir -p dirname is more elegant.

Resources