The Shell Crossover
The Shell Crossover, Part 7: Installing Software from the Terminal
Bridge MSI and EXE habits to macOS terminal installs with pkg, app, dmg, installer, hdiutil, pkgutil, ditto, and system_profiler.
Windows software installation usually starts with .msi, .exe, install switches, detection rules, and registry evidence. macOS uses different package shapes and different inspection tools.
This article focuses on local terminal installation and inspection. It is not a packaging-authoring guide and it is not a Jamf policy guide.
The format map
| Windows format | macOS counterpart | What it means |
|---|---|---|
.msi | .pkg | Installer package processed by the macOS installer system. |
.exe installer | .pkg, scripted installer, or vendor tool | There is no single universal equivalent. |
| Installed program folder | .app bundle | A directory bundle that appears as one app in Finder. |
| ISO-mounted installer | .dmg | A disk image. It may contain a .pkg, .app, or other files. |
| Add/Remove Programs evidence | package receipts and app inventory | Use pkgutil and system_profiler. |
The most important distinction: a .dmg is not itself an installer. It is a mounted disk image. You still need to install the package or copy the app inside it.
Installing a pkg
The native command for a package install is installer.
sudo installer -pkg "/path/to/Example.pkg" -target /PowerShell can call the same command.
$PkgPath = "/path/to/Example.pkg"
& /usr/bin/sudo /usr/sbin/installer -pkg $PkgPath -target /
if ($LASTEXITCODE -ne 0) {
throw "installer failed with exit code $LASTEXITCODE"
}For admin workflows, always check the vendor’s install documentation. Some packages include scripts, system extensions, helper tools, or post-install requirements.
Mounting a dmg
A disk image must be attached before you can access its contents.
hdiutil attach "/path/to/Example.dmg" -nobrowse -readonly
ls /VolumesDo not hardcode the mounted volume path in production scripts unless you control the disk image. Discover it from the hdiutil output.
For a simple teaching example, this pattern works for most vendor DMGs whose mounted volume names do not contain spaces:
volume_path=$(hdiutil attach "/path/to/Example.dmg" -nobrowse -readonly \
| awk '/\/Volumes/{print $NF}')
echo "Mounted at: $volume_path"That awk form is intentionally readable, but it is not the most robust programmatic interface. If the mounted volume name contains spaces, $NF returns only the last whitespace-delimited field, not the full mount path.
For production scripts that must handle arbitrary vendor DMGs, use hdiutil with -plist and parse the plist output instead of parsing human-readable output.
attach_output=$(hdiutil attach "/path/to/Example.dmg" -nobrowse -readonly -plist)
volume_path=""
for index in {0..20}; do
candidate=$(/usr/libexec/PlistBuddy \
-c "Print :system-entities:$index:mount-point" \
/dev/stdin 2>/dev/null <<< "$attach_output")
if [[ -n "$candidate" ]]; then
volume_path="$candidate"
break
fi
done
if [[ -z "$volume_path" ]]; then
echo "Could not determine mounted volume path." >&2
exit 1
fi
echo "Mounted at: $volume_path"The -plist pattern is longer, but it avoids guessing where the mount path appears in a formatted text line. It also handles mounted volume names with embedded spaces.
After the installation or copy is complete, detach the mounted volume.
hdiutil detach "$volume_path"If the DMG contains a package, install the package from the mounted volume.
sudo installer -pkg "$volume_path/Example.pkg" -target /
hdiutil detach "$volume_path"If the DMG contains an app bundle, copy it to /Applications.
sudo ditto "$volume_path/Example.app" "/Applications/Example.app"
hdiutil detach "$volume_path"ditto is commonly used on macOS for preserving bundle structure and metadata during copies. Be aware that ditto merges into an existing destination. For a clean reinstall, remove the existing app bundle first, after you have confirmed the target path.
sudo rm -rf "/Applications/Example.app"
sudo ditto "$volume_path/Example.app" "/Applications/Example.app"PowerShell wrapper:
$DmgPath = "/path/to/Example.dmg"
$HdiOutput = & /usr/bin/hdiutil attach $DmgPath -nobrowse -readonly
if ($LASTEXITCODE -ne 0) {
throw "hdiutil attach failed with exit code $LASTEXITCODE"
}
$VolumeMatch = $HdiOutput |
Select-String -Pattern '(/Volumes/.+)$' |
Select-Object -First 1
if (-not $VolumeMatch) {
throw "Could not determine mounted volume path from hdiutil output."
}
$VolumePath = $VolumeMatch.Matches[0].Groups[1].Value
if ([string]::IsNullOrWhiteSpace($VolumePath)) {
throw "Could not determine mounted volume path from hdiutil output."
}
try {
& /usr/bin/sudo /bin/rm -rf "/Applications/Example.app"
if ($LASTEXITCODE -ne 0) {
throw "rm failed with exit code $LASTEXITCODE"
}
& /usr/bin/sudo /usr/bin/ditto "$VolumePath/Example.app" "/Applications/Example.app"
if ($LASTEXITCODE -ne 0) {
throw "ditto failed with exit code $LASTEXITCODE"
}
}
finally {
& /usr/bin/hdiutil detach $VolumePath
}The PowerShell example captures the full /Volumes/... path with a regex group and reads that group explicitly. Do not use .Matches.Value with a pattern such as /Volumes/, because that returns the matched pattern text, not the full mount path.
Do not assume every vendor DMG mounts with the file name you expect.
Inspecting package receipts
Package receipts are the closest macOS equivalent to installer inventory evidence. Use pkgutil rather than scraping receipt files directly.
List package identifiers:
pkgutil --pkgs | grep -i exampleInspect one package receipt:
pkgutil --pkg-info com.example.packageList files recorded by a receipt:
pkgutil --files com.example.packagePowerShell version:
& /usr/sbin/pkgutil --pkgs | Where-Object { $_ -match "example" }
& /usr/sbin/pkgutil --pkg-info "com.example.package"
& /usr/sbin/pkgutil --files "com.example.package"A receipt proves that an installer registered package evidence. It does not always prove that every expected file still exists.
Inspecting installed applications
Use system_profiler for application inventory from the local Mac.
system_profiler SPApplicationsDataTypeFor shorter output:
system_profiler SPApplicationsDataType -detailLevel miniPowerShell can capture and filter text, or ask for JSON on versions that support it.
& /usr/sbin/system_profiler SPApplicationsDataType -detailLevel mini$appJson = & /usr/sbin/system_profiler SPApplicationsDataType -json
$appInventory = $appJson | ConvertFrom-Json
$appInventory.SPApplicationsDataType |
Where-Object { $_._name -match "Safari" } |
Select-Object _name, version, pathThe underscore-prefixed _name property is not a typo. It is part of the shape Apple emits in the system_profiler JSON output.
For quick filesystem checks, inspect /Applications directly.
test -d "/Applications/Example.app" && echo "Example.app exists"PowerShell version:
Test-Path -LiteralPath "/Applications/Example.app"Homebrew is not the same layer as MDM
Homebrew can be useful on admin workstations and lab systems, but it is not the same thing as managed software deployment. A fleet deployment needs source control, logging, detection, rollback planning, and alignment with your management platform.
Use Homebrew when it fits the operational model. Do not treat it as an automatic replacement for Jamf policy, MDM app deployment, Installomator, or vendor-supported package deployment.
The operating rule
Translate the installation job, not the file extension.
A .pkg gets installed with installer. A .dmg gets mounted with hdiutil, then you install or copy what is inside. An .app bundle is usually copied into /Applications. Package evidence is inspected with pkgutil, while application inventory is inspected with system_profiler or filesystem checks.