AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
셸 스크립트를 붙여넣기
Paste a shell script. Every flag that does not exist on macOS (BSD userland) or Alpine (BusyBox) is marked, with the platform's own man page line and a real execution on that platform as the proof. Sh
개발 도구TypeScript · ShellGitHub @barbarkaragul-oss온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Asked of ShellCheck since 2015, never built: paste a shell script, see every flag that breaks on macOS or BusyBox, proven by that platform's own man page and a real run.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Paste a shell script. Every flag that does not exist on macOS or BusyBox is marked, and the proof is that platform's own man page line and a real run of the binary there. Nothing is uploaded; the checks run in your browser.README 전문 보기 ↗"Works on my Linux, breaks on my Mac." sed -i without a suffix, date -d yesterday, grep -P, find -printf, xargs -d, cp --reflink, stat -c, timeout: the same dozen flags have broken macOS and Alpine CI jobs for fifteen years. ShellCheck has been asked to warn about them since 2015 (#479, #973, #1455, #2902); its maintainer's answer was that ShellCheck knows which shell you target, not which userland, and that somebody would have to maintain the list. Nobody wanted to maintain the list by hand. This page does not: the list is measured.
sed -i 's/foo/bar/' config.txt
Ubuntu · GNU ok sed -i exists; argument optional here; executed there
macOS · BSD ok sed -i exists; takes a required argument here; executed there
mdoc: .Op Fl i Ar extension ← so 's/foo/bar/' becomes the backup suffix
date -d yesterday
macOS · BSD breaks the binary answered "date: illegal option -- d" (macOS 26.6.2, run on 2026-09-16)
uniq -D hosts.txt
macOS · BSD ok executed there, exit 0 ← the folklore says macOS has no -D; macOS 26 does
Alpine · BusyBox breaks "uniq: unrecognized option: D"
&& chains, subshells, functions, loops, $(...), and unquoted heredocs. Quoted heredoc bodies stay inert.sudo, env, nohup, nice, timeout, xargs CMD, find -exec CMD, sh -c '...' with a static string: the inner command is checked, and the wrapper's own flags too.-rf is -r and -f; -n5 is -n with a value when the tool says -n takes one; --in-place=.bak is --in-place; everything after -- is an operand; tar xvf and ps aux are noted as old-style bundles, not judged.echo -e, set -o) are marked as depending on the shell, not the userland. A tool that does not exist on a platform (timeout, tac, nproc on macOS) is a break by itself.${x,,}, [[ ]], (( )), <( ), <<<, mapfile, read -p, an unquoted $var, a glob that matches nothing, cmd | read v, set -o pipefail, echo -e, local, function f: 142 constructs (143 one-liners; one only prints the shell's version), chosen by scanning 2,905 real scripts for what they actually use. The shebang decides the interpreter on each platform (#!/bin/bash is bash 3.2 on macOS; #!/bin/sh is dash on Ubuntu, bash-as-sh on macOS and BusyBox ash on Alpine; no shebang means the default interactive shell, which is zsh on a Mac), a selector overrides it, and every construct is compared with the recorded run under bash 5.2 on Ubuntu: same, differs (both run, print different things) or breaks (bash runs it, this shell errors).command -v, which, type or hash found it (if command -v timeout >/dev/null; then timeout 8 …, command -v tac && tac …), or on the right of || as a fallback (mktemp -d || mktemp -d -t x), is reported as guarded, not as a break. cmd || exit 1 is an error path, not a fallback, so cmd is still judged; an else branch is not guarded by the condition that sent control there. A recorded failure is also only carried over to a use of the same kind: head -n -1 failed on macOS, head -n 1 did not.$OPTS, "$@", ${FLAGS} and a dynamic command name are reported as not checked. Nothing is expanded.--help or man page line and the exact command that was executed, with its exit code and first line of stderr, and the platform version and date it was recorded on.The same checks run as a GitHub Action. Each flag that breaks is marked on its line in the pull request, with the platform's own answer as the message; the job summary has the table and the step fails.
name: scripts
on: [push, pull_request]
jobs:
will-it-run-on-a-mac:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: barbarkaragul-oss/will-it-run-on-a-mac@v1
with:
files: '**/*.sh'
It does not run anything on a Mac: it reads your scripts on the Linux runner and compares every flag with this repository's database, the same one the page uses. It needs no token and no permissions, and nothing leaves the runner.
| Input | Default | |
|---|---|---|
files | **/*.sh | glob patterns, one per line |
exclude | **/node_modules/**, **/.git/** | glob patterns to leave out |
platforms | macos,alpine | add ubuntu to report it too |
fail-on-break | true | false reports without failing the step |
verbose | false | also list what could not be checked ($OPTS, dynamic command names, tools not in the database) |
report-path | wiroam-report.json | the full report as JSON |
Outputs: breaks, warnings, files-checked, breaks-macos and breaks-alpine (the page's "N will break": rejected flags plus missing tools), report.
What counts: a flag the platform's binary rejected, a tool the platform does not have, and a shell construct that errors under the shell the shebang reaches there are errors. A flag that exists but whose recorded use still failed (sed -i 's/a/b/' f on macOS), a flag the documentation does not list, and a construct that prints something else are warnings. A break in a guarded command (after command -v found the tool, or as a || fallback) is counted as guarded and listed only with verbose: true. The numbers are the page's numbers: both count through src/engine/counts.ts.
This repository checks its own collector with it: the scripts in collector/ run every week on Ubuntu, macOS and Alpine, and the action has to find nothing in them that breaks.
The same check from a terminal, with no install:
npx github:barbarkaragul-oss/will-it-run-on-a-mac # every *.sh under the current directory
npx github:barbarkaragul-oss/will-it-run-on-a-mac deploy.sh ci/ # files and directories
It exits 1 when something breaks, 0 when nothing does, and 2 when it could not run; --json prints the report, --platforms macos narrows it, --no-fail always exits 0.
To show it in your README, use two badges. The first is your workflow's own status badge: it turns red when the check finds something that breaks. The second is fixed and says what the scripts are checked with; on its own it does not change colour, so it should not stand without the first. The job summary of each run prints both with your repository and workflow file filled in.
[](https://github.com/OWNER/REPO/actions/workflows/scripts.yml)
[](https://barbarkaragul-oss.github.io/will-it-run-on-a-mac/)
Nothing in the flag database is written by hand. collector/ is a GitHub Actions matrix, run weekly, on ubuntu-latest (Ubuntu 24.04, GNU coreutils 9.4), macos-latest (macOS 26.6, the BSD userland) and an alpine:3.20 container (BusyBox 1.36). On each platform it records, for 81 common tools:
| Step | What is recorded |
|---|---|
collect.sh | --help output, the usage line a BSD tool prints on an unknown option, the rendered man page and its mdoc source |
probe.sh | 87 scenario commands (sed -i 's/a/b/' f, date -d yesterday, find . -printf, …) with exit code, first stderr line and first stdout line |
shells/probe-shells.sh | the 143 one-liners in shells/probes.txt (142 constructs and a version check), each run as a one-line script under every shell the platform has (Ubuntu: bash 5.2, dash as sh, dash, zsh, ksh93; macOS: bash 3.2, bash-as-sh, zsh, ksh93; Alpine: BusyBox sh/ash, bash, zsh, dash), with exit code, first stdout line and first stderr line. zsh is recorded nine times on each platform: plain, under shwordsplit, ksharrays, nonomatch, octalzeroes, bashrematch, all five at once, emulate sh and emulate ksh, so the database can answer what a construct does under the configuration the reader has |
probe-all.sh | every short flag -a…-z, -A…-Z, -0…-9 and every long option any platform documents, executed with and without an operand file; the answer is classified as rejected or recognized by comparing it with the tool's own unknown-option wording, learned from two canary invocations ( and ); plus in , executed after a path () and classified by a third canary () |
probe-all.sh takes every tool in probe-tools.txt that the platform has (all 74 on Ubuntu, 71 on macOS, 68 on Alpine), runs each of its short and long flags twice, with and without the operand file, and runs each find primary once: 11,696 executions on Ubuntu, 11,304 on macOS and 10,654 on Alpine every week, plus 149, 143 and 137 canary runs. probe-shells.sh adds 1,859, 1,716 and 1,859 runs (143 one-liners under 13, 12 and 13 interpreter configurations), and probe.sh 87 on each platform. scripts/extract.ts turns the recordings into data/flags.json: per tool and platform, every documented flag with the verbatim line that documents it and its arity, every executed flag with one result (the two runs of a flag become one, rejected only if every run was), and the tool's version and path. That is 5,682 flag results for Ubuntu, 5,415 for macOS and 5,244 for Alpine, 16,341 in all. A tool whose canaries printed nothing (echo; zip on Ubuntu and macOS; getopt on macOS; printf on Alpine) is executed but keeps no results, because there is no rejection wording to compare its answers with. scripts/extract-shells.ts does the same for the shell recordings: data/shells.json is 143 probes × 38 interpreter configurations = 5,434 results, 2,002 of them under the 14 shells with their default options and 3,432 under the 24 zsh option settings (eight on each platform). The site loads one small file per tool. The weekly run commits the new database, so a flag that appears in a macOS release shows up as ok the next Monday, with the date.
"Recognized" is deliberately weak: it means the binary did not reject the option, not that it does the same thing. stat -f exists on both GNU and BSD and means different things; the page shows both platforms' evidence lines side by side and leaves the semantics to you.
Things the recordings pinned down that the cheat sheets get wrong or do not say:
readlink -f, realpath, sort -V, sort -h, uniq -D, wc -L, date -Iseconds, ls --color all ran with exit 0 on the macOS runner. The lists that say otherwise were true in 2015.sed -i without a suffix (macOS takes 's/foo/bar/' as the backup extension and then fails on the file name), sed -z, grep -P, date -d, stat -c, find -printf, xargs -d, du -b, cp --reflink, cp -u, ln -r, tar --wildcards: all rejected on macOS 26, with the binary's own message recorded. head -n, touch -d, install -D and base64 -w do exist there, but the Linux way of using them still fails: head -n -1 f (a negative count) answers illegal line count -- -1, touch -d '2020-01-01' f wants YYYY-MM-DDThh:mm:SS, install -D takes a directory argument (-D destdir) so install -D f d/sub/f2 prints the usage, and base64 -w0 f answers invalid argument f. timeout, tac and nproc are not there at all.find primaries, measured the same way (86 of them, executed after a path). macOS 26 rejects 15: -printf, -fprintf, -fprint, -fprint0, -fls, -regextype, -xtype, -readable, -writable, -executable, -used, -daystart, -xautofs, -warn/-nowarn; it accepts -newermt, -samefile, -uid/-gid, -wholename, -delete, -quit, -execdir, -okdir. BusyBox rejects 50 of the 86, among them -newermt and every other -newerXY, -anewer/, /, , , , /, , /, , , , ; it accepts , , , , and . GNU find 4.9.0 rejects 8: the BSD ones (, , , , , , ) and , which no runner accepts; exists but answers "This system does not provide a way to find the birth time of a file".| what you put at the top of the script | closes | breaks something that worked |
|---|---|---|
setopt shwordsplit | 2 | |
setopt ksharrays | 3 | declare -a |
setopt nonomatch | 3 | |
setopt octalzeroes | 2 | |
setopt bashrematch | 1 | |
| all five together | 11 | declare -a |
emulate sh | 13 | {1..3} brace expansion, declare -a |
emulate ksh | 9 | declare -a |
The best of them closes 13 of 47, and 33 of the 47 are closed by nothing. On the macOS runner 25 of them are errors, features none of the eight recorded settings adds: mapfile, shopt (four probes), ${x,,}, ${x^^}, ${!name}, ${!arr[@]}, ${x@Q}, ${x@U}, $EPOCHSECONDS, read -a/-p/-n/-N, compgen, complete, caller, wait -n, declare -n, type -t/-P, printf '%(%Y)T', ;;&. The other 8 run and answer differently, and none of the eight recorded settings changes the answer: ${PIPESTATUS[@]}, $FUNCNAME, $HOSTNAME, declare -p, declare -F, unset arr[1], local outside a function, export -f (an error on the Linux runners). emulate sh also has a cost that goes unmentioned: it turns brace expansion off, so set -- {1..3} leaves one argument where bash and a plain zsh both leave three. The same answers came from all three runners, which carry the same zsh 5.9.
#!/bin/sh is three different shells. dash 0.5.12 (Ubuntu's sh) errors on 83 of the 142 constructs, BusyBox ash 1.36.1 (Alpine's) on 57, and macOS's sh is bash 3.2 in POSIX mode, which errors on 22. A script that passes under one sh has been tested under one sh.date +%N is not an error, it is wrong. For date +%s%N, BusyBox on Alpine exits 0 and prints 10 digits, the seconds alone, where Ubuntu and macOS print 19; exit codes alone would have called it fine. Scenario probes record the first line of output for that reason.git clone https://github.com/barbarkaragul-oss/will-it-run-on-a-mac && cd will-it-run-on-a-mac
npm install
npm test # extractors on real recordings, verdicts, shell constructs, end-to-end script analysis
npx tsx scripts/build.ts # docs/ (the static site; serve it with any static server)
To re-record: fork, run the collect workflow (Actions → collect → Run workflow), download the three artifacts into out/, and run npx tsx scripts/extract.ts. To add a tool, add it to collector/tools.txt (and to probe-tools.txt if it is safe to run with arbitrary flags in a temporary directory).
Corpus behind the probe list. The shell constructs in collector/shells/probes.txt were chosen by counting what real, widely run bash scripts use. The repositories are listed in collector/shells/corpus.txt as owner/repo, with an optional @ref suffix naming a tag to check out; clone them into one directory and scan it:
mkdir -p corpus
for r in $(grep -v '^#' collector/shells/corpus.txt); do
repo=${r%@*}; ref=${r#*@}; dir="corpus/${repo//\//__}"
if [ "$ref" != "$r" ]; then git clone --depth 1 --branch "$ref" "https://github.com/$repo" "$dir"; else git clone --depth 1 "https://github.com/$repo" "$dir"; fi
done
npx tsx scripts/corpus-scan.ts corpus corpus-report.json
The report counts, per construct, how many scripts use it at least once and which ones the probes already measure; a construct many scripts use that no probe covers is a candidate for the next probe. The scanner skips zsh files and only reports how many it skipped, so a repository that is mostly zsh (ohmyzsh) contributes only its .sh files.
eval, no $(which sed).find is an expression language. Its primaries (-name, -printf, -newermt, …) are not read from the man pages; each one in find-primaries.txt is executed after a path on every platform and judged by that run alone. A primary not in that list is noted, not guessed.collector/shells/corpus.txt (counted by scripts/corpus-scan.ts on 2026-09-15; the checkouts of neofetch and thoughtbot/laptop were empty in that scan, so neither is in the count), plus the well-known differences; <<- heredocs cannot be a one-liner and are not probed. The three interpreters judged are the ones the shebang reaches (bash, sh, zsh, or the platform default when there is none); ksh, dash-on-Alpine and the eight zsh option settings per platform are recorded but not shown.tar xvf, ps aux) and key=value tools (dd if=) are noted, not judged.zsh -d -f, and $ENV/$BASH_ENV removed from the environment). /etc/zshenv is read by zsh whatever the flags say, so which startup files existed on the runner is recorded next to the results in out/<platform>/_shellfiles.tsv. A of your own (, , in , which is read by scripts even though is not) changes what your script does and is not in this data.Wrong verdict? Open an issue with the command and the platform; if a real run disagrees with the page, the run wins and becomes a probe.
MIT. The recordings are the output of the platforms' own tools; tree-sitter and tree-sitter-bash are MIT.
-~--wiroam-no-such-optionfind primaryfind . -newermt 2020-01-01find . -wiroamnosuch-cnewer-amin-cmin-iregex-lname-samefile-uid-gid-fstype-true-false-ls-printf-execdir-ok-delete-print0-regex-empty-quit-wholename-Btime-Bnewer-Bmin-flags-acl-xattr-xattrname-xautofs-newerBtuniq -D, split -d, ls -G, rm -I, base64 -b, which -s, date -v are rejected on Alpine; mktemp -t exists there but mktemp -t x, the macOS way of giving a prefix, answers Invalid argument; date -d exists there but does not understand yesterday.-d; the binary answers illegal option -- d. BusyBox accepts cp --reflink, grep --color and od -A that its usage text never mentions. This is why a run outranks a man page here.for f in *.tmp stops with no matches found), [ a == a ], ${x,,}, ${x^^}, ${!name}, ${!arr[@]}, mapfile, read -p, read -a, read -n, read -N, shopt with any option, declare -n, ${x@Q}, ${x@U}, $EPOCHSECONDS, type -t, type -P, compgen, complete, caller, wait -n, printf '%(%Y)T', and ;;& in a case. Nineteen more run under zsh and print something else: ${arr[1]} is the first element (zsh arrays start at 1), and so are ${arr[$i]} and the element you read back after arr+=(x); an unquoted $var is one word, not three, and IFS does not split it either; echo x | read v keeps v (the last command of a pipe is not a subshell); echo 'a\tb' prints a tab where bash prints the backslash; $((010)) is 10, not 8, and $((08)) is 8 where bash stops with value too great for base; ${PIPESTATUS[@]}, $FUNCNAME, ${BASH_REMATCH[1]} and $HOSTNAME are unset; a trap ... EXIT set inside a function fires when the function returns, not when the script ends; unset arr[1] leaves three elements; local outside a function is allowed; declare -p x prints typeset x=hi; declare -F f prints nothing; export -f is accepted and prints the function, as typeset -f would. The Linux builds of the same zsh 5.9 reject export -f instead; it is the only row where the three zsh runs differ. ShellCheck does not target zsh; this is the measured list./bin/bash Apple ships) refuses 21 of the 142: ${x,,}, ${x^^}, ${x@Q}, ${x@U}, declare -A, declare -n, declare -g, mapfile, [[ -v x ]], $EPOCHSECONDS, coproc, ;& and ;;& in a case, |&, printf '%(%Y)T', wait -n, read -N, ${x:1:-1} (a negative length), "${arr[@]}" of an empty array under set -u (arr[@]: unbound variable, the reason for the ${arr[@]+"${arr[@]}"} idiom), shopt -s globstar, caller. ${arr[-1]} is not an error there: it prints nothing. Every other construct printed what bash 5.2 printed.setoptshwordsplitksharraysnonomatch~/.zshenv~/.zshrccurl, wget, rsync, openssl, kill, pkill, hostname: 7 of the 81) are documented but not executed flag by flag.