# ios_system: a drop-in replacement for system() in iOS programs

> ios_system is a BSD licensed framework that lets programs ported to iOS keep calling system(), by shipping curated implementations of shell, archive and text commands as separate frameworks loaded on demand. This looks at how the substitution works, what it costs to integrate, and what iOS still gets in the way.

**holzschu/ios_system** — Drop-in replacement for system() in iOS programs

- Repository: https://github.com/holzschu/ios_system
- Stars: 1,074 · Forks: 184
- Language: C
- License: BSD-3-Clause
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/holzschu-ios-system

## Why the system() call fails on iOS in the first place

The problem this project addresses appears the moment you try to build a ported Unix program for iOS. When the source executes system commands with `system()` calls, the build fails at compile time with the error `error: 'system' is unavailable: not available on iOS`.

The examples in the project description are telling: vim, TeX and Python. These are programs written for a Unix host, where calling out to `ls`, `cp`, `rm`, `tar` or `gzip` is normal, and where a rewrite of every call site is a large and unwelcome piece of work. On iOS, the C library deliberately removes `system()`, because spawning a shell is exactly the kind of capability the platform does not want an app to have.

ios_system provides a drop-in replacement for `system()`. The integration is two lines at the top of a header file:

```cpp
extern int ios_system(char* cmd);
#define system ios_system
```

Then you link with the `ios_system.framework`, and your calls to `system()` are handled by the framework.

The audience is narrow and specific: a developer who is porting an existing command line oriented program to iOS, usually as part of a terminal, an editor or a document tool, and who would rather keep the original source's assumptions than rewrite them. The project states that the framework has been successfully integrated into four shells, Blink, OpenTerm, Pisth and LibTerm, an editor, iVim, and a TeX writing app, TeXable.

## Commands are separate frameworks, loaded when called

The architecture is what makes this work in a sandbox, and it is a deliberate design rather than an accident of packaging.

The commands available are defined in two dictionaries, `Resources/commandDictionary.plist` and `Resources/extraCommandsDictionary.plist`. At startup time, `ios_system` loads these dictionaries and enables the commands defined inside them. You have to add both dictionaries to the Copy Bundle Resources step in your Xcode project, which is a small but easy to miss integration step.

Each command is defined inside a framework. The framework is loaded when the command is called and released after the command exits. That is the key mechanism: an app that never calls `tar` never loads the tar framework, so the binary does not carry the whole Unix toolset. The repository holds the frameworks for the small commands, and the larger ones live in separate projects: python in `python_ios`, lua in `lua_ios`, TeX in `lib-tex`, and the network commands, nslookup, dig, host, ping and telnet, in `network_ios`, which you place with the other libraries and add to the embedded libraries of your application.

The command set spans shell commands such as `ls`, `cp` and `rm`, archive commands such as `curl`, `scp`, `sftp`, `tar`, `gzip` and `compress`, and a few interpreted languages, `python`, `lua` and `TeX`. Scripts written in one of the interpreted languages are also executed if they are in the `$PATH`.

So the trade is explicit: you get a curated Unix-flavoured environment with per-command load-on-demand, not a full POSIX userland.

## Three ways to build, from binary frameworks to xcodebuild

Installation is described in three escalating levels of effort, and the first one is the realistic option for most people.

The easy way, for Xcode 12 and above, is that `ios_system` is available as a set of binary frameworks. You add the project as a Swift Package dependency and link and embed the frameworks you need.

The semi-hard way is a single command that fetches the requirements and builds the XcFrameworks:

```bash
swift run --package-path xcfs build
```

That downloads all the requirements, `libssh2` and `openssl`, and builds all the ios_system XcFrameworks into the `.build` directory. The `xcfs/` directory in the repository is the Swift package that drives this, and the `createXcFrameworks.sh` script at the root is the packaging helper.

The hard way is Xcode itself. Open `ios_system.xcodeproj` and build, which creates the `ios_system` framework, and compile the other targets as well: `files`, `tar`, `curl`, `awk`, `shell`, `text` and `ssh_cmd`, each of which produces its own framework. The command line equivalent is:

```bash
xcodebuild -project ios_system.xcodeproj -alltargets -sdk iphoneos -configuration Release -quiet
```

If you need the interpreted language frameworks or the network commands, the documentation says to download the corresponding projects and compile them, and notes that all of them need the `ios_system` framework to compile. The repository also carries the individual sources, `shell/`, `curl/`, `tar/`, `awk/`, `files/`, `text/`, `ssh_cmd/`, plus `libarchive/`, `bsd_find/`, `bsd_diff/`, `libinfo/`, `libutil/`, `less-34/`, `wasm3/` and `getopt.c`.

## Embed the command frameworks, but do not link them

The integration instructions contain one distinction that will save an afternoon if you know it in advance, and one API that is the real answer to the sandboxing problem.

First, the linking rule. Link your application with the `ios_system.framework` framework, and embed but do not link the frameworks corresponding to the commands you need, `libtar.dylib` if you need `tar`, `libfiles.dylib` for `cp`, `rm` and `mv`. And add the two dictionaries to Copy Bundle Resources.

Second, the APIs. The simplest integration is to replace all calls to `system()` with calls to `ios_system()`. If you need more control and information, the framework exposes a set of functions:

- `initializeEnvironment()` sets environment variables to sensible defaults.
- `ios_executable(char* inputCmd)` returns true if the input command is one of the commands defined inside `ios_system`.
- `NSArray* commandsAsArray()` and `NSString* commandsAsString()` return all the commands available, if you need them for helping users.
- `NSString* getoptString(NSString* command)` returns a string containing all accepted flags for a given command, "dfiPRrvW" for `rm` for example, with letters followed by a colon if the flag cannot be combined with others.
- `NSString* operatesOn(NSString* command)` tells you what the command expects as arguments so you can auto-complete accordingly, returning "file", "directory" or "no".
- `int ios_setMiniRoot(NSString* mRoot)` lets you set the sandbox directory, so users are not exposed to files outside the sandbox. The argument is the path to a directory, it will not be possible to `cd` to directories above this one, and it returns 1 if successful and 0 if not.
- `FILE* ios_popen(const char* inputCmd, const char* type)` opens a pipe between the current command and the input command, as a drop-in replacement for `popen`.

`ios_setMiniRoot` is the one to pay attention to. It is how a host app confines the whole command environment to a directory it chooses, and it is the difference between a ported program that can read the user's documents and one that cannot leave the sandbox.

## The iOS filesystem breaks most ported programs anyway

The most useful section of the documentation is the one that is not about the framework at all. It is headed Issues, and it explains that in iOS you cannot write in the `~` directory, only in `~/Documents/`, `~/Library/` and `~/tmp`, and that most Unix programs assume the configuration files are in `$HOME`.

So the solution offered is to either redefine `$HOME` to `~/Documents/` or to set configuration variables, using `setenv`, to some other place, and the documentation says this is done in the `initializeEnvironment()` function. The author's own setup is shown as a starting point:

```
setenv PATH = $PATH:~/Library/bin:~/Documents/bin
setenv PYTHONHOME = $HOME/Library/
setenv SSH_HOME = $HOME/Documents/
setenv CURL_HOME = $HOME/Documents/
setenv HGRCPATH = $HOME/Documents/.hgrc/
setenv SSL_CERT_FILE = $HOME/Documents/cacert.pem
```

The accompanying note is Your Mileage May Vary, and the reminder that iOS already defines `$HOME` and `$PATH`.

This is worth dwelling on in an evaluation, because it is the part of the porting that `system()` has nothing to do with. A program that reads `~/.vimrc`, `~/.ssh/config` or `~/.curlrc` will not find them on iOS no matter how the command execution is arranged, and the fix is per program, in environment variables, and often has to be handed to the user. The `SSL_CERT_FILE` line is a good example of the category of problem: TLS verification needs a certificate bundle, and the app has to place one somewhere the tools will look.

There is also a mechanism for going beyond the shipped command set. `replaceCommand(NSString* commandName, int (*newFunction)(int argc, char *argv[]), bool allOccurences)` lets you replace an existing command implementation with your own, or add new commands without editing the source, with the sample `replaceCommand(@"ls", gnu_ls_main, true)` replacing all calls to `ls` with calls to `gnu_ls_main`.

## Where ios_system is the wrong tool

The limits follow from the design, and they are clear once you know it is a compatibility shim rather than a shell.

The first is that the command set is a curated list in two plist files, not a general purpose userland. A ported program that calls a command nobody thought to include simply fails, and the fix is to write the implementation and register it, which is real work. The `replaceCommand` hook exists for exactly this, but it means you own the maintenance of anything you add.

The second is that this is iOS only. The framework is an Xcode project, the platform badge says iOS 14.0 or later, and there is no macOS or Linux build.

The third is the interpreted language question. Python, Lua and TeX are not part of this repository but of three separate ones, and they are large. A team that wants a Python runtime inside an iOS app has to evaluate three more projects, each of which is a separate integration, and each of which raises the question of what an app review process makes of shipping an interpreter. The framework itself does not address that.

The fourth is that the compatibility is partial by the author's own description. The integrations each provide a Unix look-and-feel, well, mostly feel. `system()` in this framework does not have the full semantics of a shell, and the command implementations are ports rather than the originals, so behaviour differences are possible in edge cases.

The fifth is the sandbox itself, and it is the one most people underestimate. `ios_setMiniRoot` can confine the environment to a directory, and it cannot be escaped upward, which is the right default for an app, but it also means that a ported program which expects to read and write across the user's home directory will not behave as it did on a desktop. That is a limitation of iOS rather than of the framework, and it is exactly the kind of thing the Issues section is warning about.

## BSD-3-Clause terms and a steady release line

The licence is BSD-3-Clause, with the licence file at the repository root, which is a permissive licence that requires keeping the copyright notice. For a project intended to be linked into other people's applications, including commercial ones, that is the licence you would hope for.

The release line is steady. v3.0.5 was published on 2026-02-09, and both v3.0.6 and v3.0.7 were published on 2026-10-02, and the last push to the repository was on 2026-10-19. The project is not archived, and the version numbering shows the 3.x line is the current one.

The repository layout is worth noting for anyone reading the code. The core is `ios_system.m` with `ios_system.xcodeproj/` and `ios_error.h`, and there is a `libc_replacement.c` file, which is a strong hint about the approach: where the system C library functions that iOS does not provide are needed, this project provides replacements. `getopt.c` and `getopt_long.c` are there for command line parsing, `termcap.h` and `ncurses_dll.h` for terminal handling, and there is a `wasm3` directory, which points at an interpreter of some kind being bundled.

The supported platforms are listed in the top level, with per-tool directories for `awk`, `bsd_diff`, `bsd_find`, `curl`, `file_cmds`, `less-34`, `libarchive`, `libinfo`, `libutil`, `shell`, `tar`, `text` and the ssh tools, and several have an `_ios` variant directory beside them, which is the pattern of the ported implementations. The `.travis.yml` at the root and the GitHub Actions CI badge suggest both build systems have been used over the project's life.

## Conclusion

ios_system fits a developer porting a Unix tool to iOS, such as an editor, a shell or a TeX workflow, where the source already calls system() and rewriting every call site is out of scope. It does not fit an application that needs a general purpose shell, because the command set is a curated dictionary rather than a POSIX environment, and it is the wrong choice if the app has to run under App Store rules that forbid shipping interpreters, since the Python, Lua and TeX frameworks are separate and optional. Before you integrate it, read the issue section about $HOME on iOS, because that filesystem constraint will break configuration files in most ported programs regardless of the framework.

## FAQ

### How do I use system() in an iOS program?

Add two lines to your header file, `extern int ios_system(char* cmd);` and `#define system ios_system`, then link with the `ios_system.framework`. Your existing `system()` calls are then handled by the framework.

### Which commands does ios_system provide?

Shell commands such as ls, cp and rm, archive commands such as curl, scp, sftp, tar, gzip and compress, plus interpreted languages including python, lua and TeX. The enabled set is defined in `Resources/commandDictionary.plist` and `Resources/extraCommandsDictionary.plist`, which are loaded at startup.

### How do I build ios_system from source?

The quickest route is `swift run --package-path xcfs build`, which downloads libssh2 and openssl and builds the XcFrameworks. From Xcode, open `ios_system.xcodeproj`, or run `xcodebuild -project ios_system.xcodeproj -alltargets -sdk iphoneos -configuration Release -quiet` to build every framework at once.

### What is ios_setMiniRoot for?

It sets the sandbox directory so users are not exposed to files outside the sandbox. You pass the path of a directory, it will not be possible to cd to directories above that one, and it returns 1 if successful and 0 if not.

### Why does a ported program still misbehave on iOS?

Because in iOS you cannot write in the `~` directory, only in `~/Documents/`, `~/Library/` and `~/tmp`, while most Unix programs expect configuration files in `$HOME`. The project documentation suggests redefining `$HOME` or setting configuration variables with setenv, which is done in the `initializeEnvironment()` function.

## Sources

- [holzschu/ios_system on GitHub](https://github.com/holzschu/ios_system)
- [Issues](https://github.com/holzschu/ios_system/issues)
- [License: BSD-3-Clause](https://github.com/holzschu/ios_system/blob/master/LICENSE)
- [README](https://github.com/holzschu/ios_system/blob/master/README.md)
- [Releases](https://github.com/holzschu/ios_system/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/holzschu-ios-system
