This repository tests different approaches for installing executable scripts using the install(1) command, with a focus on cross-platform portability between Linux (GNU coreutils) and macOS (BSD).
Latest test run: GitHub Actions Workflow
All required portability tests PASSED on both Linux and macOS.
| Approach | Linux (Ubuntu 24.04) | macOS (Darwin 24.6) |
|---|---|---|
install -m 755 /dev/stdin <<EOF |
✅ Works | ✅ Works |
install -m 755 /dev/fd/0 <<EOF |
✅ Works | ✅ Works |
cat >file && chmod 755 |
✅ Works | ✅ Works |
tempfile + install -m 755 |
✅ Works | ✅ Works |
When replacing a file owned by another user (e.g., root-owned in user-writable directory):
| Approach | Linux | macOS | Outcome |
|---|---|---|---|
cat >file && chmod |
File updated but NOT executable | ||
install /dev/stdin |
✅ Works | ✅ Works | File replaced, executable, new owner |
tempfile + install |
✅ Works | ✅ Works | File replaced, executable, new owner |
- install command: GNU coreutils 9.4
- Location:
/usr/bin/install - Kernel: Linux 6.11.0-1018-azure
- install command: BSD install
- Location:
/usr/bin/install - OS: macOS 15.7.2
Both of these patterns work reliably across Linux and macOS:
install -m 755 /dev/stdin target <<'EOF'
#!/usr/bin/env bash
echo "script content"
EOFPros:
- Works on both Linux and macOS
- Single command (atomic)
- Properly handles ownership changes on foreign-owned files
- Clean syntax for embedding scripts
Cons:
- None identified
tmp=$(mktemp)
printf '%s\n' "$SCRIPT_CONTENT" > "$tmp"
install -m 755 "$tmp" target
rm -f "$tmp"Pros:
- Works on both Linux and macOS
- Properly handles ownership changes on foreign-owned files
- Good for content in variables
- Most traditional approach
Cons:
- Requires cleanup of temporary file
- Multiple commands (not atomic)
The pattern install -m 755 /dev/fd/0 target <<EOF also works on both platforms and is functionally equivalent to /dev/stdin.
The pattern echo "content" | install -m 755 /dev/stdin target has platform-dependent behavior:
- Linux (GNU coreutils): ✅ Works correctly
- macOS (BSD install): ❌ Fails with exit code 71
Do not use this pattern if cross-platform compatibility is needed.
The classic cat >file && chmod 755 file pattern has a critical flaw when the target file is owned by another user (e.g., root):
# If 'target' is owned by root but world-writable:
cat >target <<EOF # ✅ Succeeds (file is writable)
#!/bin/bash
echo "new content"
EOF
chmod 755 target # ❌ FAILS (you don't own the file)This leaves the file with the new content but without execute permissions, breaking the script.
Both install approaches (heredoc and tempfile) solve this problem by atomically replacing the file, including ownership.
When replacing a file owned by another user (e.g., a root-owned file in a user-writable directory):
| Method | Behavior | Result |
|---|---|---|
cat >file && chmod |
❌ chmod fails | File updated but not executable |
install /dev/stdin |
✅ Replaces file | New owner (current user), mode 755 |
tempfile + install |
✅ Replaces file | New owner (current user), mode 755 |
Both install methods properly handle ownership transfer, while cat + chmod breaks.
The test suite (test-install-portability.sh) validates:
- Basic functionality of each approach
- Proper handling of foreign-owned files (requires sudo)
- Platform-specific behaviors
Run locally:
./test-install-portability.sh- For inline script content: Use
install -m 755 /dev/stdin target <<EOF - For variable content: Use temporary file +
install -m 755 "$tmp" target - Avoid: Piped stdin (
echo | install /dev/stdin) for cross-platform code - Never use:
cat >file && chmodwhen file ownership might change
- GNU coreutils install documentation
- FreeBSD install man page (similar to macOS BSD)
- Test workflow: .github/workflows/test-install-portability.yml