Last active
May 23, 2026 13:21
-
-
Save addohm/1a630c181d1ceeb8a99bbf318864bbe0 to your computer and use it in GitHub Desktop.
Download youtube content
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ipad-util — manage files on an iOS app's Documents folder over USB. | |
| # Works with any iOS app that opts into file sharing (UIFileSharingEnabled). | |
| # | |
| # Subcommands: | |
| # put <path>... Transfer files/dirs into Documents (dirs walked manually). | |
| # get <remote> [local] Pull a file/dir from device. | |
| # ls [remote] List remote contents (default: Documents). | |
| # rm [-f] <remote>... Delete files/dirs (prompts unless -f). | |
| # mv <old> <new> Rename or move on device. | |
| # mkdir <path> Create directory on device (recursive). | |
| # sync <local-dir> Push only files not already present (name+size compare). | |
| # info Device info: model, OS, name, free space, battery. | |
| # space Free / total storage on device. | |
| # battery Battery percentage and charging state. | |
| # apps List user apps if ideviceinstaller is installed, | |
| # otherwise print common bundle IDs and how to find more. | |
| # screenshot [file.png] Capture screenshot (default: timestamped PNG). | |
| # mount [mountpoint] ifuse-mount the app's Documents folder. | |
| # unmount [mountpoint] fusermount -u. Aliases: umount, u. | |
| # status Show device, app, and mount state. | |
| # doctor Check which required/optional tools are installed. | |
| # | |
| # Global flags (before subcommand): | |
| # -a, --app <bundle> Override app bundle for this call. | |
| # -u, --udid <udid> Override device UDID. | |
| # -y, --yes Skip the "not enough space" / rm confirmation prompts. | |
| # | |
| # Configuration precedence (highest wins): | |
| # command-line flag > env var > ~/.config/ipad-util/config > built-in default | |
| # Config file is shell-sourced; recognized keys: BUNDLE, UDID, MOUNTPOINT | |
| # Env vars: IPAD_BUNDLE, IPAD_UDID, IPAD_MOUNTPOINT | |
| set -euo pipefail | |
| # --- config loading ----------------------------------------------------------- | |
| CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/ipad-util/config" | |
| if [ -f "$CONFIG_FILE" ]; then | |
| # shellcheck disable=SC1090 | |
| . "$CONFIG_FILE" | |
| fi | |
| BUNDLE="${IPAD_BUNDLE:-${BUNDLE:-org.videolan.vlc-ios}}" | |
| UDID_OVERRIDE="${IPAD_UDID:-${UDID:-}}" | |
| DEFAULT_MOUNT_BASE="${IPAD_MOUNTPOINT:-${MOUNTPOINT:-}}" | |
| # --- helpers ------------------------------------------------------------------ | |
| die() { echo "ipad-util: $*" >&2; exit 1; } | |
| warn() { echo "ipad-util: $*" >&2; } | |
| # Detect the system's package manager so install hints work on multiple distros. | |
| _pm() { | |
| if command -v dnf >/dev/null 2>&1; then echo dnf | |
| elif command -v apt-get >/dev/null 2>&1; then echo apt | |
| elif command -v pacman >/dev/null 2>&1; then echo pacman | |
| elif command -v zypper >/dev/null 2>&1; then echo zypper | |
| else echo "" | |
| fi | |
| } | |
| # Return a distro-appropriate install command for a given tool. | |
| install_hint() { | |
| local tool="$1" | |
| case "$(_pm):$tool" in | |
| # --- Fedora / RHEL / Nobara (dnf) --- | |
| dnf:afcclient|dnf:idevice_id|dnf:ideviceinfo|dnf:idevicescreenshot) | |
| echo "sudo dnf install libimobiledevice" ;; | |
| dnf:ifuse) echo "sudo dnf install ifuse" ;; | |
| dnf:fusermount) echo "sudo dnf install fuse" ;; | |
| dnf:ideviceinstaller) echo "not in Fedora repos — build from source: https://github.com/libimobiledevice/ideviceinstaller" ;; | |
| dnf:numfmt) echo "sudo dnf install coreutils" ;; | |
| dnf:jq) echo "sudo dnf install jq" ;; | |
| # --- Debian / Ubuntu (apt) --- | |
| apt:afcclient|apt:idevice_id|apt:ideviceinfo|apt:idevicescreenshot) | |
| echo "sudo apt install libimobiledevice-utils" ;; | |
| apt:ifuse) echo "sudo apt install ifuse" ;; | |
| apt:fusermount) echo "sudo apt install fuse" ;; | |
| apt:ideviceinstaller) echo "sudo apt install ideviceinstaller" ;; | |
| apt:numfmt) echo "sudo apt install coreutils" ;; | |
| apt:jq) echo "sudo apt install jq" ;; | |
| # --- Arch / Manjaro (pacman) --- | |
| pacman:afcclient|pacman:idevice_id|pacman:ideviceinfo|pacman:idevicescreenshot) | |
| echo "sudo pacman -S libimobiledevice" ;; | |
| pacman:ifuse) echo "AUR — yay -S ifuse (removed from official repos)" ;; | |
| pacman:fusermount) echo "sudo pacman -S fuse2" ;; | |
| pacman:ideviceinstaller) echo "sudo pacman -S ideviceinstaller" ;; | |
| pacman:numfmt) echo "sudo pacman -S coreutils" ;; | |
| pacman:jq) echo "sudo pacman -S jq" ;; | |
| # --- openSUSE (zypper), best-effort --- | |
| zypper:afcclient|zypper:idevice_id|zypper:ideviceinfo|zypper:idevicescreenshot) | |
| echo "sudo zypper install libimobiledevice-tools" ;; | |
| zypper:ifuse) echo "sudo zypper install ifuse" ;; | |
| zypper:fusermount) echo "sudo zypper install fuse" ;; | |
| zypper:ideviceinstaller) echo "sudo zypper install ideviceinstaller" ;; | |
| zypper:numfmt) echo "sudo zypper install coreutils" ;; | |
| zypper:jq) echo "sudo zypper install jq" ;; | |
| *) echo "install '$tool' via your package manager" ;; | |
| esac | |
| } | |
| need() { | |
| command -v "$1" >/dev/null 2>&1 && return | |
| die "missing tool: $1 ($(install_hint "$1"))" | |
| } | |
| # Non-fatal check used by `doctor`. Returns 0 if ok or optional-and-missing, 1 if required-and-missing. | |
| _doctor_check() { | |
| local tool="$1" purpose="$2" required="${3:-0}" | |
| if command -v "$tool" >/dev/null 2>&1; then | |
| printf " [ok] %-22s %s\n" "$tool" "$purpose" | |
| return 0 | |
| fi | |
| local hint; hint=$(install_hint "$tool") | |
| if [ "$required" = "1" ]; then | |
| printf " [FAIL] %-22s %s\n install: %s\n" "$tool" "$purpose" "$hint" | |
| return 1 | |
| fi | |
| printf " [skip] %-22s %s\n install: %s\n" "$tool" "$purpose" "$hint" | |
| return 0 | |
| } | |
| resolve_udid() { | |
| if [ -n "$UDID_OVERRIDE" ]; then | |
| echo "$UDID_OVERRIDE" | |
| return | |
| fi | |
| local udids | |
| mapfile -t udids < <(idevice_id -l 2>/dev/null) | |
| case "${#udids[@]}" in | |
| 0) die "no iOS device detected (plug in and trust the computer)" ;; | |
| 1) echo "${udids[0]}" ;; | |
| *) die "multiple devices detected; pass -u UDID or set IPAD_UDID. Found: ${udids[*]}" ;; | |
| esac | |
| } | |
| afc() { afcclient -u "$(resolve_udid)" --documents "$BUNDLE" "$@"; } | |
| info_key() { ideviceinfo -u "$(resolve_udid)" "$@" 2>/dev/null; } | |
| mountpoint_default() { | |
| if [ -n "$DEFAULT_MOUNT_BASE" ]; then | |
| echo "$DEFAULT_MOUNT_BASE" | |
| else | |
| echo "/tmp/ipad-${BUNDLE##*.}" | |
| fi | |
| } | |
| human_bytes() { | |
| local n="${1:-0}" | |
| # Apple reports storage in decimal (GB = 10^9), so use SI base to match what | |
| # the user sees in Settings. Binary "GiB" output would be confusing here. | |
| if command -v numfmt >/dev/null 2>&1; then | |
| numfmt --to=si --suffix=B --format=%.2f "$n" | |
| else | |
| echo "${n} B" | |
| fi | |
| } | |
| afc_size() { afc info "$1" 2>/dev/null | awk '/^st_size:/ {print $2; exit}'; } | |
| afc_is_dir() { | |
| afc info "$1" 2>/dev/null | grep -q "st_ifmt: S_IFDIR" | |
| } | |
| # afcclient `info` exits 0 even when the path doesn't exist, so check that | |
| # real attributes are present in the output instead of relying on exit status. | |
| afc_exists() { afc info "$1" 2>/dev/null | grep -q '^st_'; } | |
| # Create a remote directory and any missing parents. afcclient mkdir is not recursive. | |
| afc_mkdir_p() { | |
| local path="$1" | |
| [ -z "$path" ] && return | |
| [ "$path" = "." ] && return | |
| [ "$path" = "/" ] && return | |
| afc_exists "$path" && return | |
| afc_mkdir_p "$(dirname "$path")" | |
| afc mkdir "$path" >/dev/null 2>&1 || true | |
| } | |
| # Recursively push a local directory to a remote path. afcclient's `put -r` is | |
| # broken in some libimobiledevice versions, so we walk the tree ourselves. | |
| put_tree() { | |
| local src="$1" remote_base="$2" | |
| afc_mkdir_p "$remote_base" | |
| while IFS= read -r -d '' f; do | |
| local rel="${f#$src/}" | |
| local dest="$remote_base/$rel" | |
| if [ -d "$f" ]; then | |
| afc_mkdir_p "$dest" | |
| else | |
| echo " + $rel" | |
| afc put "$f" "$dest" | |
| fi | |
| done < <(find "$src" -mindepth 1 -print0) | |
| } | |
| # Recursively pull a remote directory to a local path. | |
| get_tree() { | |
| local remote="$1" local_dir="$2" | |
| mkdir -p "$local_dir" | |
| local entry | |
| while IFS= read -r entry; do | |
| [ -z "$entry" ] && continue | |
| local remote_path="$remote/$entry" | |
| local local_path="$local_dir/$entry" | |
| if afc_is_dir "$remote_path"; then | |
| get_tree "$remote_path" "$local_path" | |
| else | |
| echo " > $entry" | |
| afc get "$remote_path" "$local_path" | |
| fi | |
| done < <(afc ls "$remote" 2>/dev/null) | |
| } | |
| # Recursively delete a remote tree. afcclient rm is not recursive. | |
| rm_tree() { | |
| local path="$1" | |
| if afc_is_dir "$path"; then | |
| local entry | |
| while IFS= read -r entry; do | |
| [ -z "$entry" ] && continue | |
| rm_tree "$path/$entry" | |
| done < <(afc ls "$path" 2>/dev/null) | |
| fi | |
| afc rm "$path" >/dev/null 2>&1 || true | |
| } | |
| confirm() { | |
| local prompt="$1" reply | |
| if [ "${ASSUME_YES:-0}" = "1" ]; then return 0; fi | |
| read -r -p "$prompt [y/N] " reply | |
| [[ "$reply" =~ ^[Yy]$ ]] | |
| } | |
| # Sum bytes of a local file or directory tree. | |
| local_size() { | |
| local path="$1" | |
| if [ -d "$path" ]; then | |
| find "$path" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}' | |
| elif [ -f "$path" ]; then | |
| stat -c %s "$path" | |
| else | |
| echo 0 | |
| fi | |
| } | |
| # Sum bytes of a remote file or directory tree (one afc info per entry — slow on big trees). | |
| remote_size() { | |
| local path="$1" | |
| if afc_is_dir "$path"; then | |
| local entry sum=0 sz | |
| while IFS= read -r entry; do | |
| [ -z "$entry" ] && continue | |
| sz=$(remote_size "$path/$entry") | |
| sum=$((sum + sz)) | |
| done < <(afc ls "$path" 2>/dev/null) | |
| echo "$sum" | |
| else | |
| afc_size "$path" | |
| fi | |
| } | |
| # Currently-free bytes on the iOS device (matches what iPad Settings shows as free). | |
| device_free() { | |
| info_key -q com.apple.disk_usage -k AmountDataAvailable | |
| } | |
| # Free bytes on the local filesystem containing PATH. | |
| path_free() { | |
| local path="$1" | |
| [ -e "$path" ] || path=$(dirname "$path") | |
| df --output=avail -B 1 "$path" 2>/dev/null | awk 'NR==2 {gsub(/ /,""); print}' | |
| } | |
| # Pre-flight size check. Args: direction (put|get), transfer bytes, destination free bytes. | |
| # Prompts on shortfall; bails out if the user says no. | |
| preflight_size() { | |
| local direction="$1" tx="$2" dst_free="$3" | |
| [ "${tx:-0}" -gt 0 ] || return 0 | |
| local destlabel="device" | |
| [ "$direction" = "get" ] && destlabel="local destination" | |
| if [ -z "$dst_free" ] || [ "$dst_free" -le 0 ]; then | |
| warn "could not determine $destlabel free space — skipping size check" | |
| return 0 | |
| fi | |
| printf "Transfer size: %s %s free: %s\n" \ | |
| "$(human_bytes "$tx")" "$destlabel" "$(human_bytes "$dst_free")" | |
| if [ "$tx" -gt "$dst_free" ]; then | |
| local shortfall=$((tx - dst_free)) | |
| warn "$(human_bytes "$shortfall") larger than free space on $destlabel" | |
| if [ "${ASSUME_YES:-0}" = "1" ]; then | |
| warn "proceeding anyway due to -y" | |
| return 0 | |
| fi | |
| confirm "Proceed anyway?" || die "transfer aborted" | |
| fi | |
| } | |
| # --- subcommands -------------------------------------------------------------- | |
| cmd_put() { | |
| [ "$#" -gt 0 ] || die "put: need at least one path" | |
| local total=0 src sz | |
| for src in "$@"; do | |
| [ -e "$src" ] || continue | |
| sz=$(local_size "$src") | |
| total=$((total + sz)) | |
| done | |
| preflight_size put "$total" "$(device_free)" | |
| for src in "$@"; do | |
| if [ ! -e "$src" ]; then | |
| warn "'$src' not found — skipping" | |
| continue | |
| fi | |
| local name; name=$(basename "$src") | |
| if [ -d "$src" ]; then | |
| echo ">> [dir] $src/ -> Documents/$name/" | |
| put_tree "$src" "Documents/$name" | |
| else | |
| echo ">> [file] $src -> Documents/$name" | |
| afc put "$src" "Documents/$name" | |
| fi | |
| done | |
| } | |
| cmd_get() { | |
| [ "$#" -ge 1 ] || die "get: need remote path" | |
| local remote="$1" local_path="${2:-$(basename "$1")}" | |
| afc_exists "$remote" || die "get: remote path '$remote' not found" | |
| echo " measuring remote size..." | |
| local sz; sz=$(remote_size "$remote") | |
| preflight_size get "$sz" "$(path_free "$local_path")" | |
| if afc_is_dir "$remote"; then | |
| echo ">> [dir] $remote/ -> $local_path/" | |
| get_tree "$remote" "$local_path" | |
| else | |
| echo ">> [file] $remote -> $local_path" | |
| afc get "$remote" "$local_path" | |
| fi | |
| } | |
| cmd_ls() { | |
| local target="${1:-Documents}" | |
| afc ls "$target" | |
| } | |
| cmd_rm() { | |
| local force=0 | |
| if [ "${1:-}" = "-f" ]; then force=1; shift; fi | |
| [ "$#" -gt 0 ] || die "rm: need at least one remote path" | |
| for path in "$@"; do | |
| if ! afc_exists "$path"; then | |
| warn "'$path' not found — skipping" | |
| continue | |
| fi | |
| local kind="file" | |
| afc_is_dir "$path" && kind="dir" | |
| if [ "$force" -eq 0 ]; then | |
| confirm "Delete $kind '$path'?" || { echo " skipped"; continue; } | |
| fi | |
| echo ">> rm $kind $path" | |
| if [ "$kind" = "dir" ]; then | |
| rm_tree "$path" | |
| else | |
| afc rm "$path" | |
| fi | |
| done | |
| } | |
| cmd_mv() { | |
| [ "$#" -eq 2 ] || die "mv: need <old> <new>" | |
| afc mv "$1" "$2" | |
| echo "renamed: $1 -> $2" | |
| } | |
| cmd_mkdir() { | |
| [ "$#" -eq 1 ] || die "mkdir: need <path>" | |
| afc_mkdir_p "$1" | |
| echo "created: $1" | |
| } | |
| cmd_sync() { | |
| [ "$#" -eq 1 ] || die "sync: need one local directory" | |
| local src="$1" | |
| [ -d "$src" ] || die "sync: '$src' is not a directory" | |
| local base; base=$(basename "$src") | |
| local remote_dir="Documents/$base" | |
| afc_mkdir_p "$remote_dir" | |
| echo " planning sync (sizing changed files)..." | |
| local plan_total=0 plan_count=0 lsz rsz | |
| while IFS= read -r -d '' f; do | |
| local rel="${f#$src/}" | |
| local rp="$remote_dir/$rel" | |
| lsz=$(stat -c %s "$f") | |
| rsz=$(afc_size "$rp" || true) | |
| if [ -z "$rsz" ] || [ "$rsz" != "$lsz" ]; then | |
| plan_total=$((plan_total + lsz)) | |
| plan_count=$((plan_count + 1)) | |
| fi | |
| done < <(find "$src" -type f -print0) | |
| echo " $plan_count file(s) need transfer" | |
| preflight_size put "$plan_total" "$(device_free)" | |
| local pushed=0 skipped=0 | |
| while IFS= read -r -d '' local_file; do | |
| local rel="${local_file#$src/}" | |
| local remote_path="$remote_dir/$rel" | |
| local local_sz remote_sz | |
| local_sz=$(stat -c %s "$local_file") | |
| remote_sz=$(afc_size "$remote_path" || true) | |
| if [ -n "$remote_sz" ] && [ "$remote_sz" = "$local_sz" ]; then | |
| skipped=$((skipped+1)) | |
| continue | |
| fi | |
| afc_mkdir_p "$(dirname "$remote_path")" | |
| echo " + $rel" | |
| afc put "$local_file" "$remote_path" | |
| pushed=$((pushed+1)) | |
| done < <(find "$src" -type f -print0) | |
| echo "sync done: $pushed pushed, $skipped already up-to-date" | |
| } | |
| cmd_info() { | |
| need ideviceinfo | |
| local udid; udid=$(resolve_udid) | |
| local name class type version build total avail batt charging | |
| name=$(info_key -k DeviceName) | |
| class=$(info_key -k DeviceClass) | |
| type=$(info_key -k ProductType) | |
| version=$(info_key -k ProductVersion) | |
| build=$(info_key -k BuildVersion) | |
| total=$(info_key -q com.apple.disk_usage -k TotalDiskCapacity) | |
| # AmountDataAvailable is the actually-free space; TotalDataAvailable counts | |
| # purgeable caches as free, which doesn't match what iPad Settings shows. | |
| avail=$(info_key -q com.apple.disk_usage -k AmountDataAvailable) | |
| batt=$(info_key -q com.apple.mobile.battery -k BatteryCurrentCapacity 2>/dev/null || true) | |
| charging=$(info_key -q com.apple.mobile.battery -k BatteryIsCharging 2>/dev/null || true) | |
| echo "Name: $name" | |
| echo "Model: $class ($type)" | |
| echo "iOS: $version ($build)" | |
| echo "UDID: $udid" | |
| [ -n "$total" ] && echo "Storage: $(human_bytes "$avail") free of $(human_bytes "$total")" | |
| if [ -n "$batt" ]; then | |
| local cstr="" | |
| [ "$charging" = "true" ] && cstr=" (charging)" | |
| echo "Battery: ${batt}%${cstr}" | |
| fi | |
| echo "App: $BUNDLE" | |
| } | |
| cmd_space() { | |
| need ideviceinfo | |
| # Keys (in com.apple.disk_usage): | |
| # TotalDiskCapacity full capacity (matches "256 GB" on the device spec) | |
| # TotalDataCapacity storage available to user data (excludes iOS system reservation) | |
| # AmountDataAvailable actually free right now (matches what Settings shows) | |
| # TotalDataAvailable free + purgeable caches iOS can evict | |
| local total data_cap free purgeable_free | |
| total=$(info_key -q com.apple.disk_usage -k TotalDiskCapacity) | |
| data_cap=$(info_key -q com.apple.disk_usage -k TotalDataCapacity) | |
| free=$(info_key -q com.apple.disk_usage -k AmountDataAvailable) | |
| purgeable_free=$(info_key -q com.apple.disk_usage -k TotalDataAvailable) | |
| [ -n "$total" ] && [ -n "$data_cap" ] && [ -n "$free" ] || die "could not read storage info" | |
| local user_used=$((data_cap - free)) | |
| local system=$((total - data_cap)) | |
| local purgeable=$((purgeable_free - free)) | |
| local pct=$(( user_used * 100 / total )) | |
| printf "used: %s (your data)\n" "$(human_bytes "$user_used")" | |
| printf "free: %s (immediately available)\n" "$(human_bytes "$free")" | |
| printf "system: %s (iOS itself)\n" "$(human_bytes "$system")" | |
| printf "total: %s\n" "$(human_bytes "$total")" | |
| printf "%d%% used by your data\n" "$pct" | |
| if [ "$purgeable" -gt 0 ]; then | |
| printf "(plus %s of purgeable cache that iOS will evict if needed)\n" \ | |
| "$(human_bytes "$purgeable")" | |
| fi | |
| } | |
| cmd_battery() { | |
| need ideviceinfo | |
| local batt charging | |
| batt=$(info_key -q com.apple.mobile.battery -k BatteryCurrentCapacity) | |
| charging=$(info_key -q com.apple.mobile.battery -k BatteryIsCharging) | |
| [ -n "$batt" ] || die "could not read battery info" | |
| local cstr="" | |
| [ "$charging" = "true" ] && cstr=" (charging)" | |
| echo "${batt}%${cstr}" | |
| } | |
| cmd_apps() { | |
| if command -v ideviceinstaller >/dev/null 2>&1; then | |
| local udid; udid=$(resolve_udid) | |
| if ideviceinstaller -u "$udid" list -o list_user >/dev/null 2>&1; then | |
| ideviceinstaller -u "$udid" list -o list_user | |
| else | |
| ideviceinstaller -u "$udid" -l -o list_user | |
| fi | |
| echo | |
| echo "Test whether an app supports file sharing: ipad-util -a <bundle-id> ls" | |
| return | |
| fi | |
| echo "ideviceinstaller is not installed — cannot list apps directly from the device." | |
| echo " install: $(install_hint ideviceinstaller)" | |
| echo | |
| cat <<'EOF' | |
| Common file-sharing-capable apps and their bundle IDs: | |
| org.videolan.vlc-ios VLC | |
| com.firecore.infuse Infuse | |
| com.readdle.ReaddleDocsIPad Documents by Readdle | |
| com.goodnotes.GoodNotes5 GoodNotes 5 | |
| org.xbmc.kodi Kodi | |
| com.newin.nPlayerPlus nPlayer | |
| com.skyjos.fileexplorer FE File Explorer | |
| Use one with: ipad-util -a <bundle-id> <subcommand> | |
| To find the bundle ID for any iOS app: | |
| 1. Open its App Store page in a browser and copy the numeric ID from the URL. | |
| Example: https://apps.apple.com/us/app/vlc-for-mobile/id650377962 -> 650377962 | |
| 2. Look up the bundle ID via Apple's iTunes API: | |
| curl -s 'https://itunes.apple.com/lookup?id=<NUMERIC_ID>' | jq -r '.results[0].bundleId' | |
| 3. Test whether the app supports USB file sharing: | |
| ipad-util -a <bundle-id> ls | |
| If it lists files, the app opted into UIFileSharingEnabled and you can transfer to it. | |
| If you get "Could not access documents directory", it doesn't. | |
| EOF | |
| } | |
| cmd_screenshot() { | |
| need idevicescreenshot | |
| local out="${1:-screenshot-$(date +%Y%m%d-%H%M%S).png}" | |
| idevicescreenshot -u "$(resolve_udid)" "$out" | |
| echo "saved: $out" | |
| } | |
| cmd_mount() { | |
| need ifuse | |
| local mp="${1:-$(mountpoint_default)}" | |
| mkdir -p "$mp" | |
| if mountpoint -q "$mp" 2>/dev/null; then | |
| echo "already mounted at $mp" | |
| return 0 | |
| fi | |
| local udid; udid=$(resolve_udid) | |
| ifuse --documents "$BUNDLE" -u "$udid" "$mp" | |
| echo "mounted: $mp (bundle=$BUNDLE udid=$udid)" | |
| } | |
| cmd_unmount() { | |
| local mp="${1:-$(mountpoint_default)}" | |
| if ! mountpoint -q "$mp" 2>/dev/null; then | |
| echo "nothing mounted at $mp" | |
| return 0 | |
| fi | |
| fusermount -u "$mp" | |
| echo "unmounted: $mp" | |
| if [ "$mp" = "$(mountpoint_default)" ] && [ -d "$mp" ] && [ -z "$(ls -A "$mp" 2>/dev/null)" ]; then | |
| rmdir "$mp" 2>/dev/null || true | |
| fi | |
| } | |
| cmd_doctor() { | |
| local ok=0 | |
| local pm; pm=$(_pm) | |
| echo "Detected package manager: ${pm:-unknown}" | |
| echo | |
| echo "Required tools:" | |
| _doctor_check afcclient "file transfer (put/get/ls/rm/...)" 1 || ok=1 | |
| _doctor_check idevice_id "device detection" 1 || ok=1 | |
| echo | |
| echo "Optional tools:" | |
| _doctor_check ideviceinfo "info, space, battery" | |
| _doctor_check idevicescreenshot "screenshot" | |
| _doctor_check ifuse "mount / unmount" | |
| _doctor_check fusermount "unmount" | |
| _doctor_check ideviceinstaller "apps (list installed apps)" | |
| _doctor_check numfmt "human-readable sizes" | |
| _doctor_check jq "bundle-id lookup in 'apps' help" | |
| echo | |
| echo "Connectivity:" | |
| if command -v idevice_id >/dev/null 2>&1; then | |
| local udids | |
| mapfile -t udids < <(idevice_id -l 2>/dev/null) | |
| if [ "${#udids[@]}" -eq 0 ]; then | |
| echo " no device detected — plug in and tap 'Trust' on the iPad" | |
| else | |
| local u | |
| for u in "${udids[@]}"; do echo " device: $u"; done | |
| fi | |
| else | |
| echo " (cannot check — idevice_id missing)" | |
| fi | |
| echo | |
| if [ "$ok" = "0" ]; then | |
| echo "Status: ready" | |
| else | |
| echo "Status: missing required tools — see above" | |
| exit 1 | |
| fi | |
| } | |
| cmd_status() { | |
| echo "app: $BUNDLE" | |
| local udids | |
| mapfile -t udids < <(idevice_id -l 2>/dev/null) | |
| if [ "${#udids[@]}" -eq 0 ]; then | |
| echo "device(s): (none detected)" | |
| else | |
| echo "device(s): ${udids[*]}" | |
| fi | |
| local mp; mp=$(mountpoint_default) | |
| if mountpoint -q "$mp" 2>/dev/null; then | |
| echo "mount: $mp (active)" | |
| else | |
| echo "mount: $mp (inactive)" | |
| fi | |
| mount | awk '/fuse\.ifuse/ {print " also mounted: " $3}' | |
| [ -f "$CONFIG_FILE" ] && echo "config: $CONFIG_FILE" | |
| } | |
| usage() { | |
| sed -n '/^# ipad-util/,/^$/p' "$0" | sed -E 's/^# ?//' >&2 | |
| exit 2 | |
| } | |
| # --- global flag parsing ------------------------------------------------------ | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| -a|--app) BUNDLE="$2"; shift 2 ;; | |
| -u|--udid) UDID_OVERRIDE="$2"; shift 2 ;; | |
| -y|--yes) ASSUME_YES=1; shift ;; | |
| -h|--help|help) usage ;; | |
| --) shift; break ;; | |
| -*) die "unknown flag: $1 (try --help)" ;; | |
| *) break ;; | |
| esac | |
| done | |
| [ "$#" -ge 1 ] || usage | |
| sub="$1"; shift | |
| # Preflight: required tools must exist for any real subcommand. `doctor` and | |
| # `help` are exempt so the user can still diagnose a broken install. | |
| case "$sub" in | |
| doctor|help|--help|-h) ;; | |
| *) | |
| command -v afcclient >/dev/null 2>&1 || die "missing required tool: afcclient (run 'ipad-util doctor' for details)" | |
| command -v idevice_id >/dev/null 2>&1 || die "missing required tool: idevice_id (run 'ipad-util doctor' for details)" | |
| ;; | |
| esac | |
| case "$sub" in | |
| put) cmd_put "$@" ;; | |
| get) cmd_get "$@" ;; | |
| ls) cmd_ls "$@" ;; | |
| rm) cmd_rm "$@" ;; | |
| mv) cmd_mv "$@" ;; | |
| mkdir) cmd_mkdir "$@" ;; | |
| sync) cmd_sync "$@" ;; | |
| info) cmd_info ;; | |
| space|df) cmd_space ;; | |
| battery) cmd_battery ;; | |
| apps) cmd_apps ;; | |
| screenshot|shot) cmd_screenshot "$@" ;; | |
| mount|m) cmd_mount "$@" ;; | |
| unmount|umount|u) cmd_unmount "$@" ;; | |
| status) cmd_status ;; | |
| doctor) cmd_doctor ;; | |
| *) die "unknown subcommand: $sub (try: --help)" ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment