Script to automate copying from DMG

Hi Everyone. Shell scripting neophyte here.

I'm trying to create a script to a mount a DMG, copy all files from the DMG, then unmount the DMG And put it in the trash. The complication is that the mounted name of the DMG has spaces and a changing numerical suffix. I have not been able to figure out how to handle both the spaces and apply a wildcard for the numerical suffix in the same variable. If I use double quotes to bypass the spaces, then the * is parsed literally. If I don't double quote, then I can't account for the spaces in the directory name of the mounted DMG volume.

I modified a script I found on Spiceworks. The original script was dealing with a known, unchanging volume name for a specific DMG and a specific application in the DMG. I'm trying to allow for variation in the ending of the mounted volume path, and to ensure that all unhidden files of all file types are copied from the DMG to my destination.

So, for example, I have this DMG for the Dolphin emulator: dolphin-master-2407-17-universal.dmg

When I mount the DMG it will mount at: /Volumes/Dolphin\ 2407-17

The 2407-17 is a number that will change unpredictably, so I wanted to use a wild card as a substitution. The DMG contains these files:

/Volumes/Dolphin\ 2407-17/Dolphin\ Updater.app
/Volumes/Dolphin\ 2407-17/Dolphin.app

Some of my DMGs will contain text files and the names will sometimes be related to the app and sometimes will just be a README.txt or similar. So again, I wanted to use a wildcard that would just grab any file (though it would be convenient if I could limit to only visible files).

#!/bin/zsh

# Name of DMG file
dmgfile=$HOME/Downloads/dolphin-master-2407-17-universal.dmg
# Name of DMG volume
dmgvol=/Volumes/Dolphin*
# Name of files to copy
filenames=* 
# Destination for copy
filedestination=/Volumes/MX500 2TB SSD 02/Games/Emulators/Dolphin/


/usr/bin/hdiutil attach "$dmgfile" -nobrowse -quiet
cp -rf "$dmgvol/$filenames" "$filedestination/$filenames"
chmod -R 777 "$filedestination/$filenames"
/bin/sleep 5
/usr/bin/hdiutil detach "$(/bin/df | /usr/bin/grep "$dmgvol" | awk '{print $1}')" -quiet
/bin/sleep 5
/bin/rm -rf /tmp/"$dmgfile"

xattr -d com.apple.quarantine "$destination/$filenames"

exit 0

I've been trying to figure this out through reading through various forums, but I'm getting a bit lost in the nuances of expansions, substitutions, parameters and variables. Any suggestions or solutions would be much appreciated.

Welcome!

The spaces are recognized as word delimiters! They must be quoted. I recommend to quote the whole string:
filedestination="/Volumes/MX500 2TB SSD 02/Games/Emulators/Dolphin/"
The unquoted * are not expanded here because it is an assignment. Nevertheless I recommend
dmgvol="/Volumes/Dolphin*"
filenames="*"
To allow expansion (word-splitting and filename generation) the $varname must not be quoted.
Quoted things prevent the shell from expansion, but the shell dequotes them. A command won't see the quotes.
cp -rf $dmgvol/$filenames "$filedestination/"
Do not generate the destination filenames. The shell might not find them, then the * is kept, and the cp command does not do any expansion. But the cp command can take a destination directory!
chmod -R 777 "$filedestination"/$filenames

The automatic expansion of an unquoted $var is standard in all shells - but zsh. See my next post.

Here is a /bin/sh version, as I would code it. Untested of course.

#!/bin/sh
PATH=/bin:/usr/bin:/sbin:/usr/sbin
# Name of DMG file
dmgfile="$HOME/Downloads/dolphin-master-2407-17-universal.dmg"
# Name of DMG volume
dmgvolbase="/Volumes/Dolphin"
# Name of files to copy
fnmask="*"
# Destination for copy
filedestdir="/Volumes/MX500 2TB SSD 02/Games/Emulators/Dolphin"
 
if
  hdiutil attach "$dmgfile" -nobrowse -quiet
then
 
  # dmgvol becomes the last match
  for dmgvol in "$dmgvolbase"*; do :; done
  echo "$dmgfile mounted on $dmgvol"
 
  cp -rf "$dmgvol"/$fnmask "$filedestdir"/
  chmod -R 777 "$filedestdir"/$fnmask
 
  if
    hdiutil detach "$dmgfile" -quiet
  then
    echo "$dmgfile detached"
    # Further actions
  fi
  # Further actions even if the detach failed
 
else
  echo "mount of $dmgfile failed"
fi
 
exit 0

If you insist in /bin/zsh then either

  • put a line
    emulate ksh

or

  • use a = modifier to allow expansion of an unquoted $var
    ${=var} or short $=var
  cp -rf "$dmgvol"/$=fnmask "$filedestdir"/
  chmod -R 777 "$filedestdir"/$=fnmask

Thank you so much for your time and expertise, the explanation, and the revised script. I think I followed all (or at least most) of the explanation.

I'm still wrapping my head around loops. In this for loop...

 for dmgvol in "$dmgvolbase"*; do :; done

...dmgvol is a variable you create within the loop that is the equivalent of dmgvol="dmgvolbase"*, is that correct? Also, what is the meaning and function of the colon in the loop, in this context? I don't understand the comment # dmgvol becomes the last match.

I'm testing the script and I've encountered some issues. I'm trying to figure them out, but if you have any further insights I would appreciate the help.

  1. Emulate isn't working in the script.
    When I attempt to run it as a /bin/zsh script, the emulate command is not found. I tried commenting out the PATH variable you provided at the start as my PATH includes some non-standard directories due to using the Homebrew package manager for many command line tools. However, that made no difference. When I run the emulate command in Terminal directly, it works as expected. So I'm not sure why that is failing.
  2. Modifiers for ZSH variable expansion aren't working.
    I tried the other two options of replacing $fnmask with $=fnmask or ${=fnmask}, but both result in line 23: "$dmgvol"/${=fnmask}: bad substitution. So it seems like the expansion is still failing under ZSH. I just changed it to run as /bin/sh as you suggested and the expansion works properly.

It isn't critical that I can use ZSH, but I would like to understand why the alternatives you provided for using ZSH aren't working. I'm using ZSH as it is the default on macOS and scripts I use in a professional context are likely to require using the default shell. So I'm just trying to learn the nuances of ZSH as much as I can, and that way my little personal projects like this will have more value as a learning experience.

There were some issues with extended attributes, but using ditto instead of cp to copy the files resolved those concerns. Here is my updated version of your script above that seems to be working as I intended in my testing so far:

#!/bin/sh

PATH=/bin:/usr/bin:/sbin:/usr/sbin
# Name of DMG file
dmgfile="/Volumes/MX500 2TB SSD 02/Games/Unsorted/dolphin-master-2407-17-universal.dmg"
# Name of DMG volume
dmgvolbase="/Volumes/Dolphin"
# Name of files to copy
fnmask="*"
# Destination for copy
filedestdir="/Volumes/MX500 2TB SSD 02/Games/Emulators/Dolphin"

if
  hdiutil attach "$dmgfile" -nobrowse
then
 
  # dmgvol becomes the last match
  for dmgvol in "$dmgvolbase"*; do :; done
  echo "$dmgfile mounted on $dmgvol"
 
  ditto "$dmgvol" "$filedestdir"/  
 
  if
    hdiutil detach "$dmgvol"
  then
    echo "$dmgvol unmounted"
    # Further actions
  fi
  # Further actions even if the detach failed
 
else
  echo "Mount of $dmgfile failed"
fi

# Remove quarantine bit from applications in destination directory
xattr -d com.apple.quarantine "$filedestdir"/*.app

exit 0

Thanks again for the help!

I have assumed that you run your script like a binary, and this would pick a /bin/zsh according to the shebang
#!/bin/zsh
But obviously you run your script with a certain shell like

/bin/sh scripname

Then it' s run with a /bin/sh of course.

Regarding the PATH, you can append the non-standard directories, or append the PATH from the environment, like

PATH=/bin:/usr/bin:$PATH

Regarding the for loop, the shell expands the
"$dmgvolbase"*
and the loop cycles through the matching filenames, ending with the last one. The : is a no-op command.

Oh, yes I am calling it through an automation app that is running with a certain shell as you described. I can specify which shell, but it isn't running it as a command binary.

I wasn't familiar with the no-op command as a concept generally, but I did a bit of reading and now that makes sense and the purpose in loop makes sense as well.

Thanks again!