├── README.md ├── hazel-newvolume.sh └── img ├── Hazel-01b.png ├── Hazel-02.png └── Hazel-03.png /README.md: -------------------------------------------------------------------------------- 1 | hazel-newvolume 2 | =============== 3 | 4 | A shell script to work with Hazel to automatically install apps on OS X. 5 | 6 | 7 | ## The "Problem" ## 8 | 9 | Many Mac apps come in ".dmg" files, and require the same set of steps: 10 | 11 | 1. Mount the .dmg (which will then appear in **/Volumes/SomeNameHere/**) 12 | 13 | 2. Find the .app inside the **/Volumes/SomeNameHere/** 14 | 15 | 3. Copy the .app from **/Volumes/SomeNameHere/** to **/Applications/** 16 | 17 | 4. Say "yes" when asked if you want to replace it 18 | 19 | 5. Wait for the install to finish 20 | 21 | 6. Eject the .dmg 22 | 23 | ## One Solution ## 24 | 25 | Most of those steps can be automated, but there are some details which need to be considered: 26 | 27 | 1. How can you tell if a new volume is a DMG? 28 | 29 | 2. Assuming it is determined to be a DMG, how do you identify what needs to be installed? There are several possibilities, the first two is the most likely, but the others need to be considered: 30 | 31 | * There is one .app which needs to be copied to /Applications/. *(This is the most common situation.)* 32 | 33 | * it is a .pkg file which needs to be installed *(second most common)* 34 | 35 | * There is one .app but it ***is*** an installer (iTunes, Default Folder X, etc) 36 | 37 | * it is a prefPane which needs to be installed either to ~/Library/PreferencePanes/ or /Library/PreferencePanes/ 38 | 39 | * there may be an .app *and* a .pkg file (MakeMKV) 40 | 41 | * there may be more than one app in the .dmg (for example: MailMate's DMG also includes SpamSieve) 42 | 43 | * there may be more than one app and more than one pkg 44 | 45 | 3. Once we have determined what needs to be installed, there are more considerations to make: 46 | 47 | * Is the app already installed? 48 | 49 | * is it currently running? 50 | 51 | * does it have any 'helper' apps that are running? 52 | 53 | * if the app or any of its helper apps are running, can they be quit *safely* (or at least, relatively safely)? 54 | 55 | * Is the version we are considering installing newer than the currently installed version? 56 | 57 | * if no, does that mean we should *stop* the installation or just inform the user, who might *want* to replace a newer, buggier version of an app with an older version? (Our answer may depend somewhat on what we do with apps that are already installed.) 58 | 59 | 4. {Other?} 60 | 61 | 62 | ## Version 0.01 (2014-01-01) ## 63 | 64 | To start with I decided to only deal with the most common scenario: a DMG is mounted and it has an app which is not an installer. 65 | 66 | The script will be called via Hazel. Choose /Volumes/ as the Folder and then create a new rule using the "+" under “Rules”: 67 | 68 | ![](img/Hazel-01b.png) 69 | 70 | This is the rule that I created: 71 | 72 | ![](img/Hazel-02.png) 73 | 74 | "Dated Added" "is after" "Date Last Matched" means that it will run _once_ for every new item added to /Volumes/ (so the script will need to be smart enough to tell when it is not being used on a dmg). 75 | 76 | The two Display Notifications are mostly for debugging purposes, so I can tell when the recipe has started and finished. 77 | 78 | ### Faux Hazel Syncing 79 | 80 | ***Hazel*** doesn't sync its rules across Macs, *but* you can sort of fake it when you are creating a rule which points to a shell script on Dropbox. 81 | 82 | * **Create the rule on one Mac, including the "Run Shell Script" part. Instead of using an embedded script, use an external script file which is saved on Dropbox.** The full path to **`hazel-newvolume.sh`** is **`/Users/luomat/Dropbox/bin/hazel-newvolume.sh`** on all of my Macs which sync via Dropbox. You can put it wherever you want in *your* Dropbox, as long as the paths are the same on all of your Macs. (*Note:* on one computer which does not have enough internal storage for my Dropbox, I told the Dropbox.app to put its folder on **`/Volumes/External/Dropbox`** but then I *linked* it to **`/Users/luomat/Dropbox/`** .) 83 | 84 | * **Click the gear icon in Hazel and Export "Volumes" rules from Hazel.** (see image) I'd suggest putting the **`Volumes.hazelrules`** file in Dropbox to make it easier to import on your other Macs. 85 | 86 | ![](img/Hazel-03.png) 87 | 88 | * **On the other Macs, make sure you have an entry for /Volumes/ under Folders.** You can then import the **`Volumes.hazelrules`** file by either: a) using the same 'gear' menu in the above image (choose "Import Rules…"), or b) drag the **`Volumes.hazelrules`** file to the /Volumes/ item under "Folders" or c) simply double-click the **`Volumes.hazelrules`** file and choose to import it into Volumes: 89 | 90 | 91 | 92 | 93 | 94 | Once I have exported this Hazel rule on one Mac and imported it on the other Mac(s), any changes I make to the **`hazel-newvolume.sh`** script will be reflected on all of the computers which sync via Dropbox. -------------------------------------------------------------------------------- /hazel-newvolume.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh -f 2 | # Purpose: Script that acts on new volumes mounted in /Volumes/ 3 | # 4 | # From: Tj Luo.ma 5 | # Mail: luomat at gmail dot com 6 | # Web: http://RhymesWithDiploma.com 7 | # Date: 2013-12-30 8 | 9 | NAME="$0:t:r" 10 | 11 | APP_NAME='HazelHelper' 12 | BUNDLE_ID='com.noodlesoft.hazelhelper' 13 | 14 | zmodload zsh/datetime 15 | 16 | function msg { 17 | 18 | TIME=$(strftime "%Y-%m-%d @ %H.%M.%S" "$EPOCHSECONDS") 19 | 20 | if (( $+commands[terminal-notifier] )) 21 | then 22 | terminal-notifier \ 23 | -message "$@" \ 24 | -sender "${BUNDLE_ID}" \ 25 | -title "${NAME}" \ 26 | -subtitle "${TIME}" \ 27 | -execute "open -a 'Finder' '/Applications/'" 28 | fi 29 | 30 | echo "$NAME: $@" 31 | } 32 | 33 | unmount_dmg () { 34 | 35 | MNTPATH="$@" 36 | 37 | MAX_ATTEMPTS="10" 38 | 39 | SECONDS_BETWEEN_ATTEMPTS="5" 40 | 41 | # strip away anything that isn't a 0-9 digit 42 | SECONDS_BETWEEN_ATTEMPTS=$(echo "$SECONDS_BETWEEN_ATTEMPTS" | tr -dc '[0-9]') 43 | MAX_ATTEMPTS=$(echo "$MAX_ATTEMPTS" | tr -dc '[0-9]') 44 | 45 | # initialize the counter 46 | COUNT=0 47 | 48 | # NOTE this 'while' loop can be changed to something else 49 | while [ -e "$MNTPATH" ] 50 | do 51 | 52 | # increment counter (this is why we init to 0 not 1) 53 | ((COUNT++)) 54 | 55 | # check to see if we have exceeded maximum attempts 56 | if [ "$COUNT" -gt "$MAX_ATTEMPTS" ] 57 | then 58 | 59 | msg "Exceeded $MAX_ATTEMPTS" 60 | 61 | break 62 | fi 63 | 64 | # don't sleep the first time through the loop 65 | [[ "$COUNT" != "1" ]] && sleep ${SECONDS_BETWEEN_ATTEMPTS} 66 | 67 | # Do whatever you want to do in the 'while' loop here 68 | diskutil unmount "$MNTPATH" && msg "${MNTPATH} unmounted" 69 | 70 | done 71 | } 72 | 73 | 74 | pgrep -iq 'MacUpdate Desktop' && msg "Quitting because MacUpdate Desktop is running" && exit 0 75 | 76 | 77 | APP_INSTALL_DIR='/Applications' 78 | 79 | for ARGS in "$@" 80 | do 81 | case "$ARGS" in 82 | -a|--a) 83 | 84 | shift 85 | ;; 86 | 87 | -b|--b) 88 | shift 89 | ;; 90 | 91 | -*|--*) 92 | msg "[info]: Don't know what to do with arg: $1" 93 | shift 94 | ;; 95 | 96 | esac 97 | 98 | done # for args 99 | 100 | for VOLUME in "$@" 101 | do 102 | 103 | if { command mount | fgrep "$VOLUME" | sed "s#.*${VOLUME} ##g" | fgrep -q 'read-only' } 104 | then 105 | # If we get here, this a read-only disk like a DMG 106 | : 107 | 108 | else 109 | # This is not a read-only disk 110 | # Skip the rest of this and go on to the next item in the $@ if there are any 111 | continue 112 | fi 113 | 114 | # If we get here, we are in a read-only drive, probably DMG 115 | 116 | (find ${VOLUME}/* -maxdepth 1 \( -iname \*.pkg -o -iname \*.app -o -iname \*.mpkg \) -print ||\ 117 | find ${VOLUME}/* -maxdepth 2 \( -iname \*.pkg -o -iname \*.app -o -iname \*.mpkg \) -print) |\ 118 | while read line 119 | do 120 | 121 | case "$line:e:l" in 122 | app) 123 | msg "Installing $line:t" 124 | 125 | # Check to make sure the app isn't an installer, if it is, open it instead 126 | case "$line:t:r:l" in 127 | *install*) 128 | msg "$line:t is an installer" 129 | 130 | # Open the installer and WAIT for it to finish 131 | open -W "$line" 132 | exit 133 | ;; 134 | 135 | *) 136 | APPNAME="$line:t" 137 | msg "${APPNAME} is NOT an installer" 138 | 139 | # Is the app already installed? 140 | 141 | if [ -e "$APP_INSTALL_DIR/$APPNAME" ] 142 | then 143 | # Yes the app is installed already 144 | # Now we have to see if the app is running 145 | 146 | # `pgrep` is only standard in 10.8 and later I think 147 | 148 | # PIDS=$(pgrep -f "${APP_INSTALL_DIR}/${APPNAME}") 149 | 150 | PIDS=($(pgrep -d ' ' -f "${APPNAME}")) 151 | 152 | while [ "$PIDS" != "" ] 153 | do 154 | # FIXME/BUG/LIMITATION: if the app does not quit, we'll be stuck here forever 155 | PID="$PIDS[1]" 156 | 157 | for RUNNINGAPP in `ps -o command= ${PID} | tr '/' '\012' | egrep '\.app$' | tail -1 | sed -e 's/\.app$//'` 158 | do 159 | osascript -e "tell application \"$RUNNINGAPP\" to quit" 160 | sleep 5 # give it a chance to quit 161 | done 162 | 163 | # Check to see if we still have any PIDS left 164 | 165 | PIDS=($(pgrep -d ' ' -f "${APPNAME}")) 166 | 167 | done 168 | 169 | # OK, now it should be quit already, so now we need to move it out of the way 170 | 171 | 172 | # Who owns the file? 173 | 174 | zmodload zsh/stat 175 | 176 | APP_UID=`zstat +uid "$APP_INSTALL_DIR/$APPNAME"` 177 | 178 | if [ "$UID" = "$APP_UID" ] 179 | then 180 | # IS owned by current user 181 | # osascript -e "tell app \"Finder\" to delete POSIX file \"\"" 182 | 183 | osascript -e "tell application \"Finder\" to delete POSIX file \"$APP_INSTALL_DIR/$APPNAME\"" ||\ 184 | mv -f "$APP_INSTALL_DIR/$APPNAME" "$HOME/.Trash/" 185 | 186 | 187 | 188 | else 189 | # Not owned by current user 190 | # osascript -e "tell app \"Finder\" to delete POSIX file \"${PWD}/$b\"" 191 | osascript -e "tell app \"Finder\" to delete POSIX file \"$APP_INSTALL_DIR/$APPNAME\" with administrator privileges" ||\ 192 | sudo mv -f "$APP_INSTALL_DIR/$APPNAME" "$HOME/.Trash/" 193 | 194 | fi # if the app is owned by this user 195 | 196 | 197 | fi # if app is already installed 198 | 199 | # ditto -v "$MNTPNT/Spotify/Spotify.app" "/Applications/Spotify.app" 200 | 201 | if [ -e "$APP_INSTALL_DIR/$APPNAME" ] 202 | then 203 | msg "Cannot install $line:t because it still exists in $APP_INSTALL_DIR" 204 | exit 1 205 | fi 206 | 207 | # here is where we actually install the app 208 | 209 | if { command ditto "${line}" "${APP_INSTALL_DIR}/${APPNAME}" } 210 | then 211 | 212 | # Tell the user we have succeeded 213 | msg "Installed $line:t to $APP_INSTALL_DIR" 214 | 215 | else 216 | 217 | msg "Failed to install $line to $APP_INSTALL_DIR" 218 | 219 | exit 1 220 | fi 221 | 222 | 223 | 224 | # Reveal the installed file 225 | open -R "$APP_INSTALL_DIR/$APPNAME" 226 | 227 | # make sure were are not in the DMG's PATH 228 | cd / 229 | 230 | unmount_dmg "$VOLUME" 231 | 232 | ;; 233 | 234 | esac 235 | ;; 236 | 237 | pkg|mpkg) 238 | msg "[TODO] Package install of $line" 239 | 240 | ;; 241 | 242 | *) 243 | msg "[TODO]: I don't know what to do with EXT = $line:e:l" 244 | ;; 245 | 246 | esac 247 | done 248 | done 249 | 250 | 251 | 252 | exit 253 | # 254 | #EOF 255 | -------------------------------------------------------------------------------- /img/Hazel-01b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjluoma/hazel-newvolume/586a8ac370a6bab7116542750199914288b9162c/img/Hazel-01b.png -------------------------------------------------------------------------------- /img/Hazel-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjluoma/hazel-newvolume/586a8ac370a6bab7116542750199914288b9162c/img/Hazel-02.png -------------------------------------------------------------------------------- /img/Hazel-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjluoma/hazel-newvolume/586a8ac370a6bab7116542750199914288b9162c/img/Hazel-03.png --------------------------------------------------------------------------------