Skip to content

Instantly share code, notes, and snippets.

@usrbinkat
Created January 6, 2026 00:25
Show Gist options
  • Select an option

  • Save usrbinkat/e3a4350ec407afbe35c576dd73259434 to your computer and use it in GitHub Desktop.

Select an option

Save usrbinkat/e3a4350ec407afbe35c576dd73259434 to your computer and use it in GitHub Desktop.

Workspace Volume Manager for macOS

A robust utility for creating and managing a persistent APFS volume mounted at /workspace on macOS systems. This tool provides automated volume provisioning, boot-time mounting, and comprehensive lifecycle management through a single command-line interface.

Version: 10.0.0
Platform: macOS 15.2+ (Sequoia)
License: MIT


Table of Contents

  1. Overview
  2. Key Features
  3. Use Cases
  4. System Requirements
  5. Quick Start
  6. Installation
  7. Command Reference
  8. Architecture
  9. Security Considerations
  10. Troubleshooting
  11. Uninstallation
  12. Frequently Asked Questions
  13. Technical Reference

Overview

The Workspace Volume Manager (workspace.sh) creates a dedicated APFS volume mounted at the root-level path /workspace. This provides a persistent, system-independent storage location that exists outside of user home directories and standard macOS filesystem hierarchies.

Unlike traditional directory structures, the /workspace volume operates as a first-class APFS volume within the existing container, sharing storage dynamically with the system volume and other APFS volumes. This approach eliminates fixed partition sizes while maintaining complete separation from user data and system files.

The utility handles the full lifecycle of volume management: creation, configuration, automatic mounting at boot, repair operations, and clean removal. All configuration integrates with native macOS mechanisms including synthetic.conf, fstab, and launchd.


Key Features

Dynamic Storage Allocation
The workspace volume shares the APFS container's storage pool with other volumes. Storage expands and contracts automatically based on actual usage, eliminating the need to pre-allocate fixed partition sizes.

Root-Level Mount Point
The volume mounts at /workspace, providing a clean, predictable path accessible from any context. This location remains consistent regardless of which user account is active.

Automatic Boot Mounting
A LaunchDaemon ensures the volume mounts automatically during system startup. The included helper script handles boot sequence timing variations and recovers from common edge cases.

FileVault Integration
Data protection is provided through FileVault container-level encryption. When FileVault is enabled on the system, the workspace volume inherits encryption automatically without additional configuration.

Robust Error Recovery
The repair command diagnoses and corrects common issues including incorrect mount locations, missing configuration files, and boot timing problems.

Non-Destructive Uninstallation
Configuration can be removed while preserving volume data, allowing for reinstallation or migration without data loss.


Use Cases

Development Environments
Developers benefit from a consistent project location that persists across system updates, user account changes, and development tool reinstallations. Container volumes, virtual machine images, and large dependency caches remain stable at /workspace.

Research and Data Analysis
Researchers working with large datasets gain a dedicated storage location isolated from system operations. The dynamic storage allocation accommodates variable dataset sizes without manual partition management.

Multi-User Systems
On shared workstations, /workspace provides a common location accessible to authorized users regardless of which account created specific files. Standard POSIX permissions control access.

Containerized Workflows
Container runtimes such as Docker and Podman can bind mount /workspace directly, providing consistent paths between host and container environments. Development containers access source code at the same absolute path as the host system.

Educational Environments
Students and instructors benefit from a standardized project location that simplifies instructions and reduces path-related confusion across different user configurations.


System Requirements

Component Requirement
Operating System macOS 15.2 (Sequoia) or later
Privileges Administrator access (sudo)
Filesystem APFS container with available space
Encryption FileVault enabled (recommended)

The utility performs version checking at runtime and provides clear error messages if requirements are not met.

Recommended Configuration:
Enabling FileVault ensures that all data written to /workspace is encrypted at rest. Without FileVault, the volume stores data unencrypted. FileVault can be enabled in System Settings under Privacy & Security.


Quick Start

For users familiar with macOS system administration, the following commands provide rapid deployment:

# Download the script
curl -O https://raw.githubusercontent.com/containercraft/workspace-setup/main/workspace.sh
chmod +x workspace.sh

# Install (replace 'username' with the target owner)
sudo ./workspace.sh install --user username

# Reboot to activate synthetic.conf mount point
sudo reboot

# Verify installation after reboot
sudo ./workspace.sh status

The remainder of this document provides detailed explanations for each step.


Installation

Step 1: Obtain the Script

Download workspace.sh to a convenient location. The script requires no installation itself and runs directly.

curl -O https://raw.githubusercontent.com/containercraft/workspace-setup/main/workspace.sh
chmod +x workspace.sh

Step 2: Review System Status

Before installation, verify the current system state:

# Check macOS version
sw_vers

# Check FileVault status
fdesetup status

# Check existing APFS volumes
diskutil apfs list

Step 3: Run Installation

Execute the install command with the desired owner username:

sudo ./workspace.sh install --user $(whoami)

The installation process performs the following operations:

  1. Validates macOS version and system requirements
  2. Detects the primary APFS container
  3. Creates a new APFS volume named "Workspace"
  4. Configures /etc/synthetic.conf for the mount point
  5. Adds an entry to /etc/fstab for mount options
  6. Creates a helper script at /usr/local/bin/workspace-mount-helper.sh
  7. Installs a LaunchDaemon for automatic mounting
  8. Sets ownership to the specified user

Step 4: Restart the System

A restart is required for macOS to process the synthetic.conf entry and create the /workspace mount point:

sudo reboot

The synthetic.conf mechanism operates during early boot, before the standard filesystem mounts. This timing requires a full restart rather than a logout or service restart.

Step 5: Verify Installation

After reboot, confirm successful installation:

sudo ./workspace.sh status

A successful installation displays checkmarks for all components:

workspace: Checking /workspace status...

info: Verifying installation...
info: ✓ Mount point /workspace exists
info: ✓ Volume 'Workspace' found at disk3s7
info: ✓ Volume correctly mounted at /workspace
info: ✓ LaunchDaemon plist found
info: ✓ Mount helper script found
info: ✓ synthetic.conf configured
info: ✓ fstab configured
...
info: All checks passed

Installation Options

Option Description
--user <username> Required. Sets ownership of the volume.
--dry-run Shows planned operations without executing them.
--verbose Displays detailed execution information.
--force Recreates the volume if it already exists.
--no-confirm Skips interactive confirmation prompts.

Dry Run Example:

sudo ./workspace.sh install --user $(whoami) --dry-run --verbose

This displays all planned operations without modifying the system, useful for understanding the installation process before committing.


Command Reference

status

Displays comprehensive diagnostic information about the installation state.

sudo ./workspace.sh status

The status command checks:

  • Mount point existence
  • Volume presence and device identification
  • Current mount location
  • LaunchDaemon installation
  • Helper script presence
  • Configuration file entries
  • Recent log entries
  • FileVault status

No modifications are made to the system.

install

Creates and configures the workspace volume.

sudo ./workspace.sh install --user <username> [options]

Requires the --user flag to specify volume ownership. A system restart is required after installation.

uninstall

Removes workspace configuration from the system.

sudo ./workspace.sh uninstall [--purge]

Without --purge, the volume and its data are preserved. Only configuration files and the LaunchDaemon are removed. The volume can be remounted manually or reinstalled later.

With --purge, the volume and all contained data are permanently deleted along with configuration files. This operation is irreversible.

repair

Diagnoses and corrects common issues without full reinstallation.

sudo ./workspace.sh repair

The repair command handles:

  • Volume mounted at /Volumes/Workspace instead of /workspace
  • Missing or corrupted helper scripts
  • Missing LaunchDaemon configuration
  • Incomplete fstab entries

If the mount point does not exist, repair configures synthetic.conf and advises that a reboot is required.

mount

Manually mounts the workspace volume.

sudo ./workspace.sh mount

Useful for mounting after manual unmounting or when the LaunchDaemon has not yet executed. Requires the mount point to exist (i.e., the system has been rebooted after installation).

unmount

Manually unmounts the workspace volume.

sudo ./workspace.sh unmount

The volume remains available for remounting. Data is preserved.

help

Displays complete usage information.

./workspace.sh help

version

Displays the current script version.

./workspace.sh version

Architecture

Understanding the underlying architecture assists with troubleshooting and informs decisions about integration with other system components.

Component Overview

The workspace volume implementation relies on four macOS mechanisms working in coordination:

┌─────────────────────────────────────────────────────────────┐
│                    Boot Sequence                            │
├─────────────────────────────────────────────────────────────┤
│  1. Kernel processes /etc/synthetic.conf                    │
│     └─► Creates /workspace as synthetic mount point         │
│                                                             │
│  2. APFS container unlocked (FileVault if enabled)         │
│     └─► Volumes become accessible                           │
│                                                             │
│  3. launchd starts LaunchDaemon                            │
│     └─► Executes workspace-mount-helper.sh                  │
│                                                             │
│  4. Helper script mounts volume                             │
│     └─► /workspace becomes available                        │
└─────────────────────────────────────────────────────────────┘

synthetic.conf

The /etc/synthetic.conf file instructs the kernel to create synthetic filesystem entities at the root level during early boot. This mechanism enables the creation of /workspace without modifying the read-only system volume.

Entry format:

workspace

A single word on a line creates an empty directory at that path. The kernel processes this file before userspace services start, ensuring the mount point exists when the LaunchDaemon executes.

Changes to synthetic.conf require a full system restart to take effect.

fstab

The /etc/fstab entry specifies mount options for the volume:

UUID=<volume-uuid> /workspace apfs rw,noauto

The noauto option prevents the system from attempting to mount the volume before the mount point exists. The LaunchDaemon handles mounting after boot sequence completion.

LaunchDaemon

The LaunchDaemon at /Library/LaunchDaemons/com.local.workspace-mount.plist triggers the mount helper script at system startup:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.local.workspace-mount</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/workspace-mount-helper.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardErrorPath</key>
    <string>/var/log/workspace-mount-error.log</string>
    <key>StandardOutPath</key>
    <string>/var/log/workspace-mount.log</string>
</dict>
</plist>

Mount Helper Script

The helper script at /usr/local/bin/workspace-mount-helper.sh handles boot timing complexities:

  1. Waits up to 120 seconds for the mount point to exist
  2. Checks if the volume is already correctly mounted
  3. Detects if the volume auto-mounted to /Volumes/Workspace
  4. Unmounts from incorrect location if necessary
  5. Mounts the volume at /workspace

This retry logic accommodates variations in boot timing across different hardware configurations and macOS updates.

APFS Volume Characteristics

The workspace volume operates as a standard APFS volume within the existing container:

Property Behavior
Storage Allocation Dynamic, shared with container
Encryption Inherits from FileVault
Snapshots Supported
Cloning Supported
Compression Inherits container settings

The volume does not require pre-allocated space. Available storage equals the container's free space minus other volumes' usage.


Security Considerations

Data Protection

The workspace volume relies on FileVault for encryption. When FileVault is enabled:

  • Data is encrypted at rest using the container's encryption key
  • The volume unlocks automatically when the user logs in
  • No additional passwords or keys are required for the workspace volume

When FileVault is disabled:

  • Data is stored unencrypted on disk
  • Physical access to the device enables data extraction
  • The status command displays a warning about encryption status

Recommendation: Enable FileVault before creating the workspace volume to ensure all data is protected from initial creation.

Permissions

The install command sets ownership to the specified user with staff group membership:

chown <user>:staff /workspace
chmod 755 /workspace

These permissions allow:

  • Owner: full read, write, execute
  • Group (staff): read and execute
  • Others: read and execute

Adjust permissions after installation if different access controls are required:

sudo chmod 700 /workspace  # Owner only

System Integrity Protection

The utility operates correctly with System Integrity Protection (SIP) enabled. SIP should remain enabled for system security. The status command confirms SIP state.

LaunchDaemon Security

The LaunchDaemon runs with root privileges to perform mount operations. The helper script is owned by root and not writable by other users:

-rwxr-xr-x  1 root  wheel  workspace-mount-helper.sh

Troubleshooting

Mount Point Does Not Exist

Symptom: The status command reports /workspace does not exist.

Cause: The system has not been rebooted since installation, or synthetic.conf was not updated correctly.

Resolution:

  1. Verify synthetic.conf contains the workspace entry:

    cat /etc/synthetic.conf

    The output should include a line containing only workspace.

  2. If the entry exists, restart the system:

    sudo reboot
  3. If the entry is missing, run repair:

    sudo ./workspace.sh repair

    Then restart the system.

Volume Mounted at Wrong Location

Symptom: The volume mounts at /Volumes/Workspace instead of /workspace.

Cause: The automatic mount occurred before the synthetic mount point was available, or the LaunchDaemon did not execute correctly.

Resolution:

sudo ./workspace.sh repair

The repair command unmounts from the incorrect location and remounts at /workspace.

Volume Not Mounting After Reboot

Symptom: The mount point exists but the volume is not mounted.

Cause: The LaunchDaemon may not have loaded, or the helper script encountered an error.

Resolution:

  1. Check the error log:

    sudo cat /var/log/workspace-mount-error.log
  2. Attempt manual mounting:

    sudo ./workspace.sh mount
  3. If manual mounting fails, run repair:

    sudo ./workspace.sh repair

Permission Denied Errors

Symptom: Unable to write to /workspace despite the volume being mounted.

Cause: Ownership may not match the current user.

Resolution:

# Check current ownership
ls -la /workspace

# Change ownership if needed
sudo chown $(whoami):staff /workspace

Checking Logs

The mount helper script writes to two log files:

# Standard output log
sudo cat /var/log/workspace-mount.log

# Error log
sudo cat /var/log/workspace-mount-error.log

These logs include timestamps and detailed information about mount operations.


Uninstallation

Preserving Data

To remove configuration while keeping the volume and its data:

sudo ./workspace.sh uninstall

This operation:

  • Removes the LaunchDaemon
  • Removes the helper script
  • Unmounts the volume
  • Removes the fstab entry
  • Removes the synthetic.conf entry

The volume remains in the APFS container and can be accessed through /Volumes/Workspace after the next reboot, or reinstalled to /workspace.

Complete Removal

To remove the volume and all contained data permanently:

sudo ./workspace.sh uninstall --purge

Warning: This operation permanently deletes all data stored in the workspace volume. Ensure backups exist before proceeding.

Post-Uninstallation

After uninstallation, a restart is required for the synthetic.conf removal to take effect. The /workspace path will no longer exist after restart.


Frequently Asked Questions

Can the mount point be changed to something other than /workspace?

The current implementation uses /workspace as a fixed path. Modifying the mount point requires editing the script constants and reinstalling. A future version may support configurable mount points.

Does the volume survive macOS upgrades?

APFS volumes persist across macOS upgrades. However, the LaunchDaemon and helper script may require reinstallation after major upgrades. Run status after upgrading to verify, and repair if issues are detected.

How much space does the volume use?

The volume uses only the space required by its contents. APFS dynamically allocates storage from the shared container pool. Check current usage with:

diskutil info /workspace | grep "Volume Used Space"

Can multiple users access /workspace?

Yes, subject to standard POSIX permissions. Adjust permissions as needed:

# Allow all users full access
sudo chmod 777 /workspace

# Allow staff group write access
sudo chmod 775 /workspace

Is Time Machine backup supported?

Time Machine includes APFS volumes in backups by default. The workspace volume will be backed up unless explicitly excluded in Time Machine preferences.

Can the volume be encrypted separately from FileVault?

The current implementation relies on FileVault container-level encryption. Per-volume encryption would require a different APFS configuration and is not supported by this utility.

What happens if I run install twice?

Without the --force flag, the install command exits with an error if the volume already exists. With --force, the existing volume is deleted and recreated, resulting in data loss.


Technical Reference

File Locations

File Purpose
/etc/synthetic.conf Mount point creation directive
/etc/fstab Volume mount options
/Library/LaunchDaemons/com.local.workspace-mount.plist Boot-time mount trigger
/usr/local/bin/workspace-mount-helper.sh Mount execution script
/var/log/workspace-mount.log Operation log
/var/log/workspace-mount-error.log Error log

Exit Codes

Code Meaning
0 Success
1 Error (details printed to stderr)

Environment Variables

The script does not read environment variables. All configuration is performed through command-line arguments.

Dependencies

The script requires only standard macOS utilities:

  • diskutil
  • mount
  • launchctl
  • sw_vers
  • fdesetup
  • Standard shell utilities (sed, grep, awk)

No additional software installation is required.

APFS Container Detection

The script automatically detects the primary APFS container by parsing diskutil apfs list output. On systems with multiple APFS containers, the first container is used. Custom container selection is not currently supported.

Boot Timing

The helper script implements a 120-second timeout waiting for the mount point. This duration accommodates systems with encrypted volumes, network dependencies, or other factors that may delay boot completion. The timeout can be adjusted by editing the max_retries variable in the helper script.


Contributing

Contributions are welcome. Please submit issues and pull requests to the project repository.

License

This project is released under the MIT License. See the LICENSE file for details.


Documentation version: 10.0.0
*Last updated: Jan 2026

#!/bin/bash
#
# workspace.sh - Manage /workspace APFS volume on macOS
# Version: 10.0.0
#
# This script creates and manages an APFS volume mounted at /workspace
# using synthetic.conf, fstab, and LaunchDaemon integration.
# Data protection is provided by FileVault container-level encryption.
#
# Key improvements in v10:
# - Robust boot sequence handling with retry logic
# - Automatic detection and remounting from /Volumes/Workspace
# - Repair command for fixing common issues
# - Enhanced diagnostics and status checking
# - Better handling of synthetic.conf timing issues
#
set -euo pipefail
readonly VERSION="10.0.0"
readonly SCRIPT_NAME="workspace.sh"
readonly VOLUME_NAME="Workspace"
readonly MOUNT_POINT="/workspace"
readonly PLIST_PATH="/Library/LaunchDaemons/com.local.workspace-mount.plist"
readonly HELPER_SCRIPT="/usr/local/bin/workspace-mount-helper.sh"
readonly SYNTHETIC_CONF="/etc/synthetic.conf"
readonly FSTAB_PATH="/etc/fstab"
readonly LOG_FILE="/var/log/workspace-mount.log"
readonly ERROR_LOG_FILE="/var/log/workspace-mount-error.log"
# Global flags
DRY_RUN=false
VERBOSE=false
FORCE=false
NO_CONFIRM=false
PURGE=false
INSTALL_USER=""
#
# Logging and utility functions
#
info() {
echo "info: $*" >&2
}
warning() {
echo "warning: $*" >&2
}
error() {
echo "error: $*" >&2
}
fatal() {
echo "error: $*" >&2
exit 1
}
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
fatal "Required command not found: $1"
fi
}
check_root() {
if [ "$EUID" -ne 0 ]; then
fatal "This command must be run with sudo"
fi
}
#
# System verification functions
#
check_macos_version() {
local version
version=$(sw_vers -productVersion)
local major minor
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
info "Detected macOS $version"
if [ "$major" -lt 15 ] || { [ "$major" -eq 15 ] && [ "$minor" -lt 2 ]; }; then
fatal "macOS 15.2 (Sequoia) or later required, found $version"
fi
}
check_sip() {
if csrutil status | grep -q "enabled"; then
info "System Integrity Protection is enabled (recommended)"
return 0
else
warning "System Integrity Protection is disabled"
return 1
fi
}
check_filevault() {
local fv_status
fv_status=$(fdesetup status)
if echo "$fv_status" | grep -q "FileVault is On"; then
info "FileVault is enabled (data protection active)"
return 0
else
warning "FileVault is not enabled - volume data will not be encrypted"
warning "Enable FileVault in System Settings > Privacy & Security for data protection"
return 1
fi
}
#
# APFS volume detection functions
#
get_apfs_container() {
local output container
output=$(diskutil apfs list 2>/dev/null)
container=$(echo "$output" | grep "APFS Container Reference:" | head -n1 | awk '{print $NF}')
if [ -z "$container" ]; then
return 1
fi
echo "$container"
}
get_volume_device() {
local volume_name="$1"
local device
device=$(diskutil apfs list 2>/dev/null | awk -v name="$volume_name" '
/^[ \t]*\+-> Volume disk[0-9]+s[0-9]+/ {
current_device = $3
}
/Name:/ && $0 ~ name {
if (current_device != "") {
print current_device
exit
}
}
')
if [ -z "$device" ]; then
return 1
fi
echo "$device"
}
get_volume_uuid() {
local volume_device="$1"
local uuid
uuid=$(diskutil info "$volume_device" 2>/dev/null | grep "Volume UUID:" | awk '{print $NF}')
if [ -z "$uuid" ]; then
return 1
fi
echo "$uuid"
}
get_volume_mount_point() {
local volume_device="$1"
local mount_info
mount_info=$(diskutil info "$volume_device" 2>/dev/null | grep "Mount Point:" | awk -F: '{print $2}' | xargs)
if [ -z "$mount_info" ] || [ "$mount_info" = "Not Mounted" ]; then
return 1
fi
echo "$mount_info"
}
volume_exists() {
local volume_name="$1"
diskutil apfs list 2>/dev/null | grep -q "Name:.*${volume_name}"
}
is_mounted() {
local mount_point="$1"
mount 2>/dev/null | grep -q " on ${mount_point} "
}
#
# Configuration file management
#
create_synthetic_conf() {
local mount_point="$1"
local workspace_name
workspace_name=$(basename "$mount_point")
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would add '${workspace_name}' to ${SYNTHETIC_CONF}"
return 0
fi
if [ -f "$SYNTHETIC_CONF" ] && grep -q "^${workspace_name}$" "$SYNTHETIC_CONF" 2>/dev/null; then
if [ "$VERBOSE" = true ]; then
info "synthetic.conf already configured"
fi
return 0
fi
if [ "$VERBOSE" = true ]; then
info "Adding ${workspace_name} to ${SYNTHETIC_CONF}"
fi
touch "$SYNTHETIC_CONF"
echo "$workspace_name" >> "$SYNTHETIC_CONF"
}
remove_synthetic_conf() {
local mount_point="$1"
local workspace_name
workspace_name=$(basename "$mount_point")
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would remove '${workspace_name}' from ${SYNTHETIC_CONF}"
return 0
fi
if [ ! -f "$SYNTHETIC_CONF" ]; then
return 0
fi
if grep -q "^${workspace_name}$" "$SYNTHETIC_CONF" 2>/dev/null; then
if [ "$VERBOSE" = true ]; then
info "Removing ${workspace_name} from ${SYNTHETIC_CONF}"
fi
sed -i '' "/^${workspace_name}$/d" "$SYNTHETIC_CONF"
fi
}
create_fstab_entry() {
local volume_uuid="$1"
local mount_point="$2"
local fstab_line="UUID=$volume_uuid $mount_point apfs rw,noauto"
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would add fstab entry: $fstab_line"
return 0
fi
if [ -f "$FSTAB_PATH" ] && grep -q "$volume_uuid" "$FSTAB_PATH" 2>/dev/null; then
if [ "$VERBOSE" = true ]; then
info "fstab already configured for this volume"
fi
return 0
fi
if [ "$VERBOSE" = true ]; then
info "Adding fstab entry for $mount_point"
fi
touch "$FSTAB_PATH"
echo "$fstab_line" >> "$FSTAB_PATH"
}
remove_fstab_entry() {
local mount_point="$1"
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would remove fstab entry for ${mount_point}"
return 0
fi
if [ ! -f "$FSTAB_PATH" ]; then
return 0
fi
if grep -q "$mount_point" "$FSTAB_PATH" 2>/dev/null; then
if [ "$VERBOSE" = true ]; then
info "Removing fstab entry for $mount_point"
fi
sed -i '' "\\#${mount_point}#d" "$FSTAB_PATH"
fi
}
#
# Helper script and LaunchDaemon creation
#
create_mount_helper() {
local volume_device="$1"
local mount_point="$2"
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would create mount helper script at $HELPER_SCRIPT"
return 0
fi
if [ "$VERBOSE" = true ]; then
info "Creating mount helper script"
fi
cat > "$HELPER_SCRIPT" <<'HELPER_EOF'
#!/bin/bash
#
# workspace-mount-helper.sh - Helper script for mounting workspace volume
# This script handles the complexity of boot sequence timing
#
set -euo pipefail
VOLUME_DEVICE="$1"
MOUNT_POINT="$2"
LOG_FILE="/var/log/workspace-mount.log"
ERROR_LOG_FILE="/var/log/workspace-mount-error.log"
DEFAULT_MOUNT="/Volumes/Workspace"
log_info() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $*" >> "$LOG_FILE"
}
log_error() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $*" >> "$ERROR_LOG_FILE"
}
# Main execution
log_info "Starting workspace mount operation for device $VOLUME_DEVICE"
# Step 1: Wait for mount point to exist (up to 120 seconds)
# The synthetic.conf mount point is created early in boot but timing varies
log_info "Waiting for mount point $MOUNT_POINT to exist..."
retry_count=0
max_retries=120
while [ ! -d "$MOUNT_POINT" ] && [ $retry_count -lt $max_retries ]; do
sleep 1
retry_count=$((retry_count + 1))
if [ $((retry_count % 10)) -eq 0 ]; then
log_info "Still waiting for mount point... ($retry_count seconds elapsed)"
fi
done
if [ ! -d "$MOUNT_POINT" ]; then
log_error "Mount point $MOUNT_POINT does not exist after waiting ${max_retries} seconds"
log_error "synthetic.conf may not have been processed. A reboot is required."
exit 1
fi
log_info "Mount point $MOUNT_POINT exists"
# Step 2: Check if already mounted at the correct location
if mount | grep -q " on $MOUNT_POINT "; then
log_info "Volume already mounted at $MOUNT_POINT"
exit 0
fi
# Step 3: Check if volume is mounted at default location and unmount
if mount | grep -q " on $DEFAULT_MOUNT "; then
log_info "Volume is mounted at $DEFAULT_MOUNT, unmounting..."
if ! /usr/sbin/diskutil unmount "$DEFAULT_MOUNT" >> "$LOG_FILE" 2>> "$ERROR_LOG_FILE"; then
log_error "Failed to unmount volume from $DEFAULT_MOUNT"
exit 1
fi
log_info "Successfully unmounted from $DEFAULT_MOUNT"
sleep 2
fi
# Step 4: Mount the volume at the correct location
log_info "Mounting $VOLUME_DEVICE at $MOUNT_POINT..."
if ! /sbin/mount -t apfs "$VOLUME_DEVICE" "$MOUNT_POINT" >> "$LOG_FILE" 2>> "$ERROR_LOG_FILE"; then
log_error "Failed to mount volume at $MOUNT_POINT"
exit 1
fi
log_info "Successfully mounted volume at $MOUNT_POINT"
exit 0
HELPER_EOF
# Substitute the actual device in the script
sed -i '' "s|VOLUME_DEVICE=\"\$1\"|VOLUME_DEVICE=\"${volume_device}\"|g" "$HELPER_SCRIPT"
sed -i '' "s|MOUNT_POINT=\"\$2\"|MOUNT_POINT=\"${mount_point}\"|g" "$HELPER_SCRIPT"
chmod 755 "$HELPER_SCRIPT"
chown root:wheel "$HELPER_SCRIPT"
}
remove_mount_helper() {
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would remove mount helper script"
return 0
fi
if [ -f "$HELPER_SCRIPT" ]; then
if [ "$VERBOSE" = true ]; then
info "Removing mount helper script"
fi
rm -f "$HELPER_SCRIPT"
fi
}
create_launch_daemon() {
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would create LaunchDaemon at $PLIST_PATH"
return 0
fi
if [ "$VERBOSE" = true ]; then
info "Creating LaunchDaemon plist"
fi
cat > "$PLIST_PATH" <<PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.workspace-mount</string>
<key>ProgramArguments</key>
<array>
<string>${HELPER_SCRIPT}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>${ERROR_LOG_FILE}</string>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
</dict>
</plist>
PLIST_EOF
chmod 644 "$PLIST_PATH"
chown root:wheel "$PLIST_PATH"
}
remove_launch_daemon() {
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would remove LaunchDaemon"
return 0
fi
if [ -f "$PLIST_PATH" ]; then
if [ "$VERBOSE" = true ]; then
info "Unloading LaunchDaemon"
fi
launchctl bootout system "$PLIST_PATH" 2>/dev/null || true
if [ "$VERBOSE" = true ]; then
info "Removing LaunchDaemon plist"
fi
rm -f "$PLIST_PATH"
fi
}
remove_logs() {
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would remove log files"
return 0
fi
if [ "$VERBOSE" = true ]; then
info "Removing log files"
fi
rm -f "$LOG_FILE" "$ERROR_LOG_FILE"
}
#
# Command implementations
#
cmd_status() {
echo "workspace: Checking $MOUNT_POINT status..."
echo
info "Verifying installation..."
local checks_failed=0
# Check mount point
if [ -d "$MOUNT_POINT" ]; then
info "✓ Mount point $MOUNT_POINT exists"
else
warning "✗ Mount point $MOUNT_POINT does not exist"
info " → Reboot required after installation for synthetic.conf to take effect"
checks_failed=$((checks_failed + 1))
fi
# Check volume existence
if volume_exists "$VOLUME_NAME"; then
local device
if device=$(get_volume_device "$VOLUME_NAME"); then
info "✓ Volume '$VOLUME_NAME' found at $device"
# Check where it's mounted
local current_mount
if current_mount=$(get_volume_mount_point "$device"); then
if [ "$current_mount" = "$MOUNT_POINT" ]; then
info "✓ Volume correctly mounted at $MOUNT_POINT"
else
warning "✗ Volume mounted at $current_mount instead of $MOUNT_POINT"
info " → Run 'sudo $0 repair' to fix"
checks_failed=$((checks_failed + 1))
fi
else
warning "✗ Volume exists but is not mounted"
info " → Run 'sudo $0 mount' to mount manually"
checks_failed=$((checks_failed + 1))
fi
else
warning "✗ Volume '$VOLUME_NAME' found but device detection failed"
checks_failed=$((checks_failed + 1))
fi
else
warning "✗ Volume '$VOLUME_NAME' not found"
checks_failed=$((checks_failed + 1))
fi
# Check LaunchDaemon
if [ -f "$PLIST_PATH" ]; then
info "✓ LaunchDaemon plist found"
else
warning "✗ LaunchDaemon plist not found"
checks_failed=$((checks_failed + 1))
fi
# Check helper script
if [ -f "$HELPER_SCRIPT" ]; then
info "✓ Mount helper script found"
else
warning "✗ Mount helper script not found"
checks_failed=$((checks_failed + 1))
fi
# Check synthetic.conf
if [ -f "$SYNTHETIC_CONF" ] && grep -q "^$(basename "$MOUNT_POINT")$" "$SYNTHETIC_CONF" 2>/dev/null; then
info "✓ synthetic.conf configured"
else
warning "✗ synthetic.conf not configured"
checks_failed=$((checks_failed + 1))
fi
# Check fstab
if [ -f "$FSTAB_PATH" ] && grep -q "$MOUNT_POINT" "$FSTAB_PATH" 2>/dev/null; then
info "✓ fstab configured"
else
warning "✗ fstab not configured"
checks_failed=$((checks_failed + 1))
fi
# Check logs
echo
info "Recent log entries:"
if [ -f "$LOG_FILE" ]; then
tail -n 5 "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
echo " $line"
done
else
info " No log file found"
fi
if [ -f "$ERROR_LOG_FILE" ] && [ -s "$ERROR_LOG_FILE" ]; then
echo
warning "Recent errors:"
tail -n 5 "$ERROR_LOG_FILE" 2>/dev/null | while IFS= read -r line; do
echo " $line"
done
fi
echo
check_filevault
echo
if [ "$checks_failed" -eq 0 ]; then
info "All checks passed"
return 0
else
warning "$checks_failed check(s) failed"
if [ -d "$MOUNT_POINT" ]; then
info "Try running: sudo $0 repair"
else
info "A reboot is required for synthetic.conf changes to take effect"
fi
return 1
fi
}
cmd_install() {
if [ -z "$INSTALL_USER" ]; then
fatal "Username required. Use --user <username>"
fi
if ! id "$INSTALL_USER" >/dev/null 2>&1; then
fatal "User '$INSTALL_USER' does not exist"
fi
echo "workspace: Installing $MOUNT_POINT APFS volume..."
echo
check_macos_version
check_sip
check_filevault
info "Detecting APFS container..."
local container
if ! container=$(get_apfs_container); then
fatal "Failed to detect APFS container"
fi
if [ "$VERBOSE" = true ]; then
info "Using APFS container: $container"
fi
# Check if volume already exists
if volume_exists "$VOLUME_NAME" && [ "$FORCE" = false ]; then
fatal "Volume '$VOLUME_NAME' already exists. Use --force to recreate"
fi
echo
info "Installation summary:"
info " Container: $container"
info " Volume name: $VOLUME_NAME"
info " Mount point: $MOUNT_POINT"
info " Owner: $INSTALL_USER"
info " Encryption: FileVault container-level (if enabled)"
echo
if [ "$NO_CONFIRM" = false ] && [ "$DRY_RUN" = false ]; then
read -p "Continue with installation? [y/N] " -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "workspace: Installation cancelled"
exit 0
fi
fi
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would perform the following operations:"
fi
# Remove existing volume if --force specified
if volume_exists "$VOLUME_NAME" && [ "$FORCE" = true ]; then
info "Removing existing volume..."
if [ "$DRY_RUN" = false ]; then
local existing_device
if existing_device=$(get_volume_device "$VOLUME_NAME"); then
local existing_mount
if existing_mount=$(get_volume_mount_point "$existing_device"); then
diskutil unmount "$existing_mount" 2>/dev/null || true
fi
fi
diskutil apfs deleteVolume "$VOLUME_NAME" 2>/dev/null || true
sleep 2
fi
fi
# Create APFS volume
info "Creating APFS volume..."
local volume_device
local volume_uuid
local temp_mount
if [ "$DRY_RUN" = false ]; then
diskutil apfs addVolume "$container" APFS "$VOLUME_NAME"
sleep 3
volume_device=$(get_volume_device "$VOLUME_NAME")
if [ -z "$volume_device" ]; then
fatal "Failed to get device for volume '$VOLUME_NAME'"
fi
volume_uuid=$(get_volume_uuid "$volume_device")
if [ -z "$volume_uuid" ]; then
fatal "Failed to get UUID for volume '$VOLUME_NAME'"
fi
temp_mount=$(get_volume_mount_point "$volume_device" || echo "")
if [ "$VERBOSE" = true ]; then
info "Volume device: $volume_device"
info "Volume UUID: $volume_uuid"
if [ -n "$temp_mount" ]; then
info "Temporary mount point: $temp_mount"
fi
fi
else
volume_device="disk3sX"
volume_uuid="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
temp_mount="/Volumes/Workspace"
fi
# Set ownership
info "Setting ownership..."
if [ "$DRY_RUN" = false ]; then
if [ -n "$temp_mount" ] && [ -d "$temp_mount" ]; then
chown "$INSTALL_USER:staff" "$temp_mount"
chmod 755 "$temp_mount"
if [ "$VERBOSE" = true ]; then
info "Ownership set on $temp_mount"
fi
fi
fi
# Configure synthetic.conf
info "Configuring synthetic.conf..."
create_synthetic_conf "$MOUNT_POINT"
# Configure fstab
info "Configuring fstab..."
create_fstab_entry "$volume_uuid" "$MOUNT_POINT"
# Create mount helper script
info "Creating mount helper script..."
create_mount_helper "$volume_device" "$MOUNT_POINT"
# Create LaunchDaemon
info "Creating LaunchDaemon..."
create_launch_daemon
echo
if [ "$DRY_RUN" = true ]; then
info "Dry run complete. No changes were made."
else
info "Installation complete!"
echo
if [ -n "$temp_mount" ]; then
info "The volume '$VOLUME_NAME' is currently mounted at: $temp_mount"
else
info "The volume '$VOLUME_NAME' has been created."
fi
info "Configuration files have been created for automatic mounting at $MOUNT_POINT"
echo
info "IMPORTANT: A restart is required for the following reasons:"
info " 1. synthetic.conf changes require reboot to create $MOUNT_POINT"
info " 2. After restart, the LaunchDaemon will automatically mount the volume"
info " 3. The mount helper script includes retry logic to handle timing issues"
echo
info "After reboot, verify the installation with:"
info " sudo $0 status"
echo
if ! fdesetup status | grep -q "FileVault is On"; then
warning "FileVault is not enabled on this system"
info "Data stored in $MOUNT_POINT will not be encrypted at rest"
info "Enable FileVault in System Settings > Privacy & Security"
fi
fi
}
cmd_uninstall() {
echo "workspace: Uninstalling $MOUNT_POINT..."
echo
if [ "$PURGE" = true ]; then
warning "This will remove the volume, all its contents, and all configuration!"
else
warning "This will remove configuration but preserve the volume data!"
info "Use --purge to also delete the volume and all its contents."
fi
if [ "$NO_CONFIRM" = false ] && [ "$DRY_RUN" = false ]; then
read -p "Are you sure you want to continue? [y/N] " -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "workspace: Uninstall cancelled"
exit 0
fi
fi
if [ "$DRY_RUN" = true ]; then
info "[DRY-RUN] Would perform the following operations:"
fi
# Remove LaunchDaemon
info "Removing LaunchDaemon..."
remove_launch_daemon
# Remove mount helper script
info "Removing mount helper script..."
remove_mount_helper
# Unmount volume
if is_mounted "$MOUNT_POINT"; then
info "Unmounting volume from $MOUNT_POINT..."
if [ "$DRY_RUN" = false ]; then
diskutil unmount "$MOUNT_POINT" 2>/dev/null || true
fi
fi
# Handle volume at default location
if volume_exists "$VOLUME_NAME"; then
local device
if device=$(get_volume_device "$VOLUME_NAME"); then
local current_mount
if current_mount=$(get_volume_mount_point "$device"); then
if [ "$current_mount" != "$MOUNT_POINT" ]; then
info "Unmounting volume from $current_mount..."
if [ "$DRY_RUN" = false ]; then
diskutil unmount "$current_mount" 2>/dev/null || true
fi
fi
fi
fi
fi
# Delete volume if --purge
if [ "$PURGE" = true ]; then
if volume_exists "$VOLUME_NAME"; then
info "Deleting volume and all contents..."
if [ "$DRY_RUN" = false ]; then
diskutil apfs deleteVolume "$VOLUME_NAME"
fi
fi
info "Removing logs..."
remove_logs
else
info "Volume preserved (use --purge to delete)"
fi
# Remove fstab entry
info "Removing fstab entry..."
remove_fstab_entry "$MOUNT_POINT"
# Remove synthetic.conf entry
info "Removing synthetic.conf entry..."
remove_synthetic_conf "$MOUNT_POINT"
echo
if [ "$DRY_RUN" = true ]; then
info "Dry run complete. No changes were made."
else
info "Uninstall complete!"
info "A restart is required for synthetic.conf changes to take effect."
if [ "$PURGE" = false ]; then
info "The volume '$VOLUME_NAME' has been preserved and can be remounted."
fi
fi
}
cmd_mount() {
check_root
if ! volume_exists "$VOLUME_NAME"; then
fatal "Volume '$VOLUME_NAME' not found"
fi
if is_mounted "$MOUNT_POINT"; then
info "Volume already mounted at $MOUNT_POINT"
exit 0
fi
local device
device=$(get_volume_device "$VOLUME_NAME")
if [ -z "$device" ]; then
fatal "Failed to get device for volume '$VOLUME_NAME'"
fi
# Check if mount point exists
if [ ! -d "$MOUNT_POINT" ]; then
fatal "Mount point $MOUNT_POINT does not exist. A reboot is required."
fi
info "Mounting volume..."
mount -t apfs "$device" "$MOUNT_POINT"
info "Volume mounted at $MOUNT_POINT"
}
cmd_unmount() {
check_root
if ! is_mounted "$MOUNT_POINT"; then
info "Volume not currently mounted at $MOUNT_POINT"
# Check if mounted elsewhere
if volume_exists "$VOLUME_NAME"; then
local device
if device=$(get_volume_device "$VOLUME_NAME"); then
local current_mount
if current_mount=$(get_volume_mount_point "$device"); then
info "Volume is mounted at: $current_mount"
info "Unmounting..."
diskutil unmount "$current_mount"
info "Volume unmounted"
exit 0
fi
fi
fi
exit 0
fi
info "Unmounting volume from $MOUNT_POINT..."
diskutil unmount "$MOUNT_POINT"
info "Volume unmounted"
}
cmd_repair() {
check_root
echo "workspace: Repairing $MOUNT_POINT configuration..."
echo
if ! volume_exists "$VOLUME_NAME"; then
fatal "Volume '$VOLUME_NAME' not found. Cannot repair."
fi
local device volume_uuid
device=$(get_volume_device "$VOLUME_NAME")
if [ -z "$device" ]; then
fatal "Failed to get device for volume '$VOLUME_NAME'"
fi
volume_uuid=$(get_volume_uuid "$device")
if [ -z "$volume_uuid" ]; then
fatal "Failed to get UUID for volume '$VOLUME_NAME'"
fi
info "Volume device: $device"
info "Volume UUID: $volume_uuid"
echo
# Check and fix mount point
if [ ! -d "$MOUNT_POINT" ]; then
warning "Mount point $MOUNT_POINT does not exist"
info "This means synthetic.conf has not been processed yet"
info "Ensuring synthetic.conf is configured..."
create_synthetic_conf "$MOUNT_POINT"
info "A reboot is required for this change to take effect"
info "After reboot, run this repair command again"
exit 0
fi
# Fix fstab if needed
if [ ! -f "$FSTAB_PATH" ] || ! grep -q "$volume_uuid" "$FSTAB_PATH" 2>/dev/null; then
info "Fixing fstab configuration..."
create_fstab_entry "$volume_uuid" "$MOUNT_POINT"
fi
# Recreate helper script if missing or incorrect
if [ ! -f "$HELPER_SCRIPT" ]; then
info "Recreating mount helper script..."
create_mount_helper "$device" "$MOUNT_POINT"
fi
# Recreate LaunchDaemon if missing
if [ ! -f "$PLIST_PATH" ]; then
info "Recreating LaunchDaemon..."
create_launch_daemon
info "Loading LaunchDaemon..."
launchctl bootstrap system "$PLIST_PATH" 2>/dev/null || true
fi
# Handle volume mounted at wrong location
local current_mount
if current_mount=$(get_volume_mount_point "$device"); then
if [ "$current_mount" != "$MOUNT_POINT" ]; then
info "Volume is mounted at $current_mount, remounting to $MOUNT_POINT..."
diskutil unmount "$current_mount" 2>/dev/null || true
sleep 2
fi
fi
# Mount if not mounted
if ! is_mounted "$MOUNT_POINT"; then
info "Mounting volume at $MOUNT_POINT..."
mount -t apfs "$device" "$MOUNT_POINT"
info "Volume mounted successfully"
else
info "Volume already correctly mounted at $MOUNT_POINT"
fi
echo
info "Repair complete!"
info "Verify with: sudo $0 status"
}
show_help() {
cat <<EOF
$SCRIPT_NAME - Manage $MOUNT_POINT APFS volume on macOS
VERSION: $VERSION
USAGE:
sudo ./$SCRIPT_NAME <command> [options]
COMMANDS:
install Create and configure $MOUNT_POINT volume
uninstall Remove $MOUNT_POINT configuration
status Check installation status and diagnostics
repair Fix common issues without reinstalling
mount Manually mount the volume
unmount Manually unmount the volume
help Show this help message
version Show version information
INSTALL OPTIONS:
--user <username> Username that will own $MOUNT_POINT (required)
UNINSTALL OPTIONS:
--purge Also delete the volume and all its contents
GLOBAL OPTIONS:
--dry-run Show what would be done without making changes
--verbose Show detailed command execution
--force Force operation even if volume exists
--no-confirm Skip confirmation prompts
-h, --help Show this help message
-v, --version Show version information
EXAMPLES:
Install $MOUNT_POINT for current user:
sudo ./$SCRIPT_NAME install --user \$(whoami)
Check installation status with diagnostics:
sudo ./$SCRIPT_NAME status
Fix mounting issues after reboot:
sudo ./$SCRIPT_NAME repair
Uninstall configuration but keep volume:
sudo ./$SCRIPT_NAME uninstall
Uninstall and delete everything:
sudo ./$SCRIPT_NAME uninstall --purge
ARCHITECTURE:
$MOUNT_POINT is implemented as an APFS volume using:
- synthetic.conf for root-level mount point creation
- APFS volume in existing container (dynamic storage)
- FileVault for container-level encryption
- LaunchDaemon for automatic mounting at boot
- Robust helper script with retry logic for timing issues
- fstab for mount options (noauto)
The volume shares storage space dynamically with other volumes
in the same APFS container (no fixed size allocation).
BOOT SEQUENCE:
1. Kernel boots and processes synthetic.conf early in boot
2. Mount point $MOUNT_POINT is created as a synthetic entity
3. LaunchDaemon starts and runs helper script
4. Helper script waits up to 120 seconds for mount point
5. Helper detects if volume auto-mounted to /Volumes/Workspace
6. Helper unmounts from wrong location if needed
7. Helper mounts volume to $MOUNT_POINT
8. System is ready with volume at correct location
TROUBLESHOOTING:
If volume not mounting:
1. Check status: sudo ./$SCRIPT_NAME status
2. Try repair: sudo ./$SCRIPT_NAME repair
3. Check logs: sudo tail /var/log/workspace-mount-error.log
If mount point doesn't exist:
1. Verify synthetic.conf: grep workspace /etc/synthetic.conf
2. Reboot is required after synthetic.conf changes
If volume at wrong location:
1. Run: sudo ./$SCRIPT_NAME repair
2. This will remount to correct location
REQUIREMENTS:
- macOS 15.2+ (Sequoia or later)
- Administrator privileges (sudo)
- APFS container with available space
- FileVault enabled (recommended for data protection)
FILES:
$SYNTHETIC_CONF
$FSTAB_PATH
$PLIST_PATH
$HELPER_SCRIPT
$LOG_FILE
$ERROR_LOG_FILE
For more information, visit: https://github.com/containercraft/workspace-setup
EOF
}
show_version() {
echo "$SCRIPT_NAME version $VERSION"
}
#
# Main entry point
#
main() {
need_cmd diskutil
need_cmd sw_vers
if [ $# -eq 0 ]; then
show_help
exit 1
fi
local command=""
while [ $# -gt 0 ]; do
case "$1" in
install|uninstall|status|repair|mount|unmount)
command="$1"
shift
;;
help|-h|--help)
show_help
exit 0
;;
version|-v|--version)
show_version
exit 0
;;
--user)
INSTALL_USER="$2"
shift 2
;;
--purge)
PURGE=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--force)
FORCE=true
shift
;;
--no-confirm)
NO_CONFIRM=true
shift
;;
*)
fatal "Unknown option: $1"
;;
esac
done
if [ -z "$command" ]; then
fatal "No command specified. Use '$SCRIPT_NAME help' for usage information."
fi
case "$command" in
status)
cmd_status
;;
install)
check_root
cmd_install
;;
uninstall)
check_root
cmd_uninstall
;;
repair)
cmd_repair
;;
mount)
cmd_mount
;;
unmount)
cmd_unmount
;;
*)
fatal "Unknown command: $command"
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment