← BACK TO HOME

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 formatmacOS counterpartWhat it means
.msi.pkgInstaller package processed by the macOS installer system.
.exe installer.pkg, scripted installer, or vendor toolThere is no single universal equivalent.
Installed program folder.app bundleA directory bundle that appears as one app in Finder.
ISO-mounted installer.dmgA disk image. It may contain a .pkg, .app, or other files.
Add/Remove Programs evidencepackage receipts and app inventoryUse 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 /Volumes

Do 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 example

Inspect one package receipt:

pkgutil --pkg-info com.example.package

List files recorded by a receipt:

pkgutil --files com.example.package

PowerShell 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 SPApplicationsDataType

For shorter output:

system_profiler SPApplicationsDataType -detailLevel mini

PowerShell 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, path

The 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.