# How device_utils.py Constructs and Executes IDB Commands for Simulator Interaction

> Learn how device_utils.py builds and runs IDB commands for iOS simulator interaction. Discover the process of constructing argument lists and executing them with subprocess.run.

- Repository: [Conor/ios-simulator-skill](https://github.com/conorluddy/ios-simulator-skill)
- Tags: internals
- Published: 2026-02-27

---

**The `build_idb_command` function in [`device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/device_utils.py) constructs IDB commands as argument lists by splitting operation strings, appending positional arguments, and optionally injecting `--udid` flags, then returns the list for `subprocess.run` execution.**

The `conorluddy/ios-simulator-skill` repository provides Python utilities for automating iOS simulators via Facebook's IDB (iOS Device Bridge). At the heart of this automation lies [`device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/device_utils.py), which centralizes how **device_utils.py constructs and executes IDB commands** through a single helper function that eliminates shell-parsing ambiguity.

## The build_idb_command Helper Function

All IDB interactions in the repository flow through `build_idb_command` defined in [`ios-simulator-skill/scripts/common/device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/ios-simulator-skill/scripts/common/device_utils.py). This function transforms high-level operation descriptions into executable argument lists suitable for Python's `subprocess` module.

### Constructing the Base Command

The function begins by splitting the operation string and prefixing it with the `idb` binary:

```python
cmd = ["idb"] + operation.split()

```

This approach converts a string like `"ui tap"` into the list `["idb", "ui", "tap"]`. According to the source code at lines 71-73, this split ensures that multi-word IDB subcommands are properly separated into distinct arguments without requiring the caller to pre-split strings.

### Appending Positional Arguments

Next, the function extends the command with any additional positional arguments passed via `*args`:

```python
cmd.extend(str(arg) for arg in args)

```

As shown at lines 74-76, this generator expression converts every argument to a string before appending, allowing callers to pass integers (like screen coordinates) or other types safely. For example, coordinates `200` and `400` become string elements in the final command list.

### Injecting the Device Identifier

The function conditionally appends the `--udid` flag only when a specific device identifier is provided:

```python
if udid:
    cmd.extend(["--udid", udid])

```

Lines 78-80 implement this logic, allowing the same helper to target either a specific simulator (when `udid` is provided) or the default booted device (when `udid` is `None`). This eliminates the need for callers to handle flag presence logic repeatedly.

## Executing Commands with subprocess

The `build_idb_command` function returns a complete argument list (lines 81-82), but does not execute the command itself. Execution happens in the calling functions using `subprocess.run` with strict error handling.

For example, the `get_device_screen_size` function (lines 18-22) demonstrates this pattern:

```python
cmd = build_idb_command("ui describe-all", udid, "--json")
result = subprocess.run(
    cmd, capture_output=True, text=True, check=True
)

```

This separation of concerns ensures that [`device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/device_utils.py) remains a pure command-builder while individual modules handle execution context, output parsing, and error recovery. Using `check=True` ensures that IDB failures raise `CalledProcessError` immediately, preventing silent failures during simulator interactions.

## Practical Usage Examples

The following patterns demonstrate how `build_idb_command` handles various simulator automation scenarios across the codebase.

### Tapping on the Booted Simulator

When no UDID is specified, IDB defaults to the currently booted simulator:

```python
from ios_simulator_skill.scripts.common.device_utils import build_idb_command
import subprocess

cmd = build_idb_command("ui tap", None, "200", "400")

# Result: ["idb", "ui", "tap", "200", "400"]

subprocess.run(cmd, check=True)

```

### Targeting a Specific Device by UDID

For multi-device scenarios, pass a specific UDID to isolate the target:

```python
udid = "A1B2C3D4-5678-90AB-CDEF-1234567890AB"
cmd = build_idb_command("ui tap", udid, "250", "300")

# Result: ["idb", "ui", "tap", "250", "300", "--udid", "A1B2C3D4-5678-90AB-CDEF-1234567890AB"]

subprocess.run(cmd, check=True)

```

### Retrieving the Accessibility Tree

Complex queries like dumping the UI hierarchy use additional flags:

```python
import json

def fetch_accessibility_tree(udid: str) -> dict:
    cmd = build_idb_command("ui describe-all", udid, "--json")
    result = subprocess.run(
        cmd, capture_output=True, text=True, check=True
    )
    return json.loads(result.stdout)

```

## Integration Across the Codebase

The `build_idb_command` helper serves as the single source of truth for IDB interactions throughout the repository. Rather than constructing raw shell strings, specialized modules delegate to this utility:

- **[`ios-simulator-skill/scripts/gesture.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/ios-simulator-skill/scripts/gesture.py)** – Constructs tap and swipe commands via `build_idb_command` before executing touch events.
- **[`ios-simulator-skill/scripts/keyboard.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/ios-simulator-skill/scripts/keyboard.py)** – Builds text-entry commands using the same helper to ensure consistent argument handling.
- **[`ios-simulator-skill/scripts/navigator.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/ios-simulator-skill/scripts/navigator.py)** – Generates UI-description queries (`ui describe-all`) through the centralized builder.

This architecture eliminates shell-injection risks and ensures that device-targeting logic (the `--udid` flag) behaves identically whether performing gestures, typing text, or querying accessibility hierarchies.

## Summary

- **`build_idb_command`** in [`device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/device_utils.py) serves as the central factory for IDB argument lists, converting string operations into properly split command components.
- The function handles **operation splitting**, **positional argument conversion**, and **optional UDID injection** without shell string concatenation.
- Execution occurs via **`subprocess.run`** in calling modules, using the returned list with `check=True` for strict error handling.
- This pattern appears consistently across **gesture.py**, **keyboard.py**, and **navigator.py**, providing a single source of truth for simulator automation.

## Frequently Asked Questions

### What is IDB and why does device_utils.py use it?

IDB (iOS Device Bridge) is a command-line tool developed by Meta for automating iOS simulators and devices. The [`device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/device_utils.py) module uses IDB because it provides robust access to UI automation features like tapping, swiping, and accessibility tree inspection that are not available through Apple's `simctl` alone.

### How does build_idb_command handle special characters in arguments?

The function converts all positional arguments to strings using `str(arg)` and extends the command list directly without shell interpolation. Because it returns a list rather than a string, special characters like spaces or quotes are treated as literal arguments by `subprocess.run`, eliminating injection vulnerabilities and parsing errors common with shell strings.

### Can I use device_utils.py with physical iOS devices?

Yes, provided the physical device is provisioned for development and accessible via IDB. The `build_idb_command` function accepts a `udid` parameter that works for both simulator UUIDs and physical device identifiers. When a UDID is passed, the function appends `--udid` to the command, allowing IDB to target the specific hardware device rather than the booted simulator.

### Where does the subprocess execution happen if not in device_utils.py?

The actual `subprocess.run` calls occur in specialized modules that import the builder, such as [`ios-simulator-skill/scripts/gesture.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/ios-simulator-skill/scripts/gesture.py), [`keyboard.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/keyboard.py), and [`navigator.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/navigator.py). Additionally, higher-level utilities within [`device_utils.py`](https://github.com/conorluddy/ios-simulator-skill/blob/main/device_utils.py) itself—like `get_device_screen_size` (lines 18-22)—execute the commands immediately after building them, demonstrating that execution can occur both within the utility module and in dependent scripts.