Running External Commands from Dyalog APL with ⎕SH and ⎕CMD
How to invoke external programs from Dyalog APL using ⎕SH and ⎕CMD, capture and clean their output, trap failures, and avoid the performance and injection pitfalls.
17 Sept 2026, 03:34 UTC

When you need to call an external program from Dyalog APL — a compression tool, a Git command, a script — the supported mechanism is the system command interface: ⎕SH on Linux and macOS, and ⎕CMD on Windows. Both pass a command line to the host operating system's shell and return the program's standard output as character data. The useful takeaway up front: it works well for occasional orchestration tasks, but it spawns a full shell process per call, captures only stdout by default, and is a command-injection hazard if you interpolate untrusted input. Treat it as glue, not as a compute engine.
How the call works
You pass a single character vector containing the command line, exactly as you would type it in a shell. The interpreter starts the platform shell (/bin/sh on Unix-like systems, cmd.exe on Windows), waits for the command to finish, and returns whatever the program wrote to standard output as a nested or simple character result, depending on version and variant settings.
A minimal example, run in a Dyalog session on Linux (no special permissions needed beyond what the invoked command itself requires):
⎕SH 'echo Hello World'
Hello World
⎕SH 'date +%Y-%m-%d'
2026-10-13On Windows the equivalent uses ⎕CMD, and because the underlying shell is cmd.exe, quoting rules differ. A directory listing, for example:
⎕CMD 'dir /b C:\temp'Note the spelling: the function names are ⎕SH and ⎕CMD. Some informal notes refer to a generic "⎕S" interface; that is not the documented name in current Dyalog versions, so check the Dyalog help for your installed version (⎕VERSION will tell you what you are running) before relying on any alias.
A worked example: checking a Git commit hash
A realistic use case is embedding build information into an APL application. Suppose your deployment script wants the short Git hash of the working tree:
GetHash←{
out←⎕SH 'git -C ',⍵,' rev-parse --short HEAD'
out~⎕UCS 10 13 ⍝ strip trailing CR/LF
}
hash←GetHash '/home/deploy/myapp'Two things are happening here that matter in practice. First, the trailing newline that git prints is part of the captured output, so the function strips carriage return and line feed characters (⎕UCS 10 13). Forgetting this is the single most common beginner mistake — the hash looks right in the session but fails string comparisons downstream. Second, the working directory is passed explicitly with git -C rather than relying on the shell's current directory, which makes the function insensitive to where the APL process was started.
Error handling
If the command exits with a non-zero status, the interface signals an error rather than returning partial output silently. Wrap calls in ::Trap (or guard them with ⎕TRAP in older code) so a failed external tool does not tear down your application:
:Trap 0
out←⎕SH 'some-tool --flag'
:Else
⎕←'command failed: ',⎕DM
out←''
:EndTrapCheck the error message text for the exit code, and log it — "the command failed" without the code is nearly useless when debugging a deployment at 2 a.m.
Limits and common mistakes
- Standard error is not captured. Only stdout comes back. If you need error text, redirect it in the command string, e.g.
⎕SH 'mytool 2>&1'on Unix or the same redirection undercmd.exe. - Performance. Each call forks a shell and a child process. That is fine for a handful of calls, but hundreds of invocations inside a loop will be dominated by process-startup overhead. If you find yourself calling
⎕SHper row of a table, restructure: batch the work into one command, or move the logic into APL or a shared library. - Injection risk. Never concatenate user-supplied or file-derived strings directly into the command line. An argument containing
; rm -rf …will be interpreted by the shell. Validate against an allowlist (for example, a filename pattern) or pass data through files instead of the command line. - Quoting differences. A command tested on Linux may break on Windows because
cmd.exehandles quotes and escapes differently. If your code must run on both, isolate the command strings behind a platform check (⎕SE.Dyalog.Platformor equivalent) rather than sprinkling conditionals through call sites. - Blocking behavior. The call waits for completion. Long-running commands freeze the interpreter thread; there is no built-in async variant, so run slow jobs as detached processes (e.g. with
nohup … &) if you must not block.
Verifying it works
Three quick checks in a session confirm the setup: run ⎕SH 'echo Test' (or ⎕CMD 'echo Test' on Windows) and confirm the result is character data containing Test; deliberately run a failing command such as ⎕SH 'false' and confirm an error is signalled and trappable; and time a loop of repeated calls with ⎕AI or a timer to see the per-call overhead before committing to a design that depends on it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.