| 1 | #!/usr/bin/env bash
|
| 2 | #
|
| 3 | # Create reports, published at https://pages.oils.pub/
|
| 4 | #
|
| 5 | # Usage:
|
| 6 | # ./spec-compat-html.sh <function name>
|
| 7 | #
|
| 8 | # TODO:
|
| 9 | # - Deploy HTML
|
| 10 | # - add tree html
|
| 11 | # - improve pages.oils.pub/ index.html
|
| 12 | # - Epoch or Build timestamp on page
|
| 13 | # - Improve
|
| 14 | # - summary/percentages in TOP.html?
|
| 15 | # - More shells: might as well include ash, dash, ysh
|
| 16 | # - Make a container with `buildah`, so others can collaborate?
|
| 17 | # - Refactor
|
| 18 | # - maybe clean up test/spec-runner.sh arguments
|
| 19 |
|
| 20 | : ${LIB_OSH=stdlib/osh}
|
| 21 | source $LIB_OSH/bash-strict.sh
|
| 22 | source $LIB_OSH/task-five.sh
|
| 23 |
|
| 24 | REPO_ROOT=$(cd "$(dirname $0)/.."; pwd)
|
| 25 |
|
| 26 | source benchmarks/common.sh # cmark function
|
| 27 | source test/common.sh
|
| 28 | source test/spec-common.sh
|
| 29 | source test/tsv-lib.sh # tsv-row
|
| 30 | source web/table/html.sh # table-sort-begin
|
| 31 |
|
| 32 | # Matches SHELLS in test/spec-compat.sh
|
| 33 | readonly -a SH_LABELS=(bash dash ash zsh mksh ksh toysh sush brush osh)
|
| 34 |
|
| 35 | summary-tsv-row() {
|
| 36 | ### Print one row or the last total row
|
| 37 |
|
| 38 | local report=$1
|
| 39 | local spec_subdir=$2
|
| 40 | shift 2
|
| 41 |
|
| 42 | if test $# -eq 1; then
|
| 43 | local spec_name=$1
|
| 44 | local -a tsv_files=( _tmp/spec/$spec_subdir/$spec_name.result.tsv )
|
| 45 | else
|
| 46 | local spec_name='TOTAL'
|
| 47 | local -a tsv_files=( "$@" )
|
| 48 | fi
|
| 49 |
|
| 50 | awk -v report=$report -v spec_name=$spec_name '
|
| 51 | # skip the first row
|
| 52 | FNR != 1 {
|
| 53 | case_num = $1
|
| 54 | sh = $2
|
| 55 | result = $3
|
| 56 |
|
| 57 | if (sh == "bash") {
|
| 58 | bash[result] += 1
|
| 59 | } else if (sh == "dash") {
|
| 60 | dash[result] += 1
|
| 61 | } else if (sh == "ash") {
|
| 62 | ash[result] += 1
|
| 63 | } else if (sh == "zsh") {
|
| 64 | zsh[result] += 1
|
| 65 | } else if (sh == "mksh") {
|
| 66 | mksh[result] += 1
|
| 67 | } else if (sh == "ksh") {
|
| 68 | ksh[result] += 1
|
| 69 | } else if (sh == "toysh") {
|
| 70 | toysh[result] += 1
|
| 71 | } else if (sh == "sush") {
|
| 72 | sush[result] += 1
|
| 73 | } else if (sh == "brush") {
|
| 74 | brush[result] += 1
|
| 75 | } else if (sh == "osh") {
|
| 76 | osh[result] += 1
|
| 77 | }
|
| 78 | }
|
| 79 |
|
| 80 | END {
|
| 81 | if (spec_name == "TOTAL") {
|
| 82 | href = ""
|
| 83 | } else {
|
| 84 | href = sprintf("%s.html", spec_name)
|
| 85 | }
|
| 86 |
|
| 87 | if (report == "PASSING") {
|
| 88 | bash_total = ("pass" in bash) ? bash["pass"] : 0
|
| 89 | dash_total = ("pass" in dash) ? dash["pass"] : 0
|
| 90 | ash_total = ("pass" in ash) ? ash["pass"] : 0
|
| 91 | zsh_total = ("pass" in zsh) ? zsh["pass"] : 0
|
| 92 | mksh_total = ("pass" in mksh) ? mksh["pass"] : 0
|
| 93 | ksh_total = ("pass" in ksh) ? ksh["pass"] : 0
|
| 94 | toysh_total = ("pass" in toysh) ? toysh["pass"] : 0
|
| 95 | sush_total = ("pass" in sush) ? sush["pass"] : 0
|
| 96 | brush_total = ("pass" in brush) ? brush["pass"] : 0
|
| 97 | osh_total = ("pass" in osh) ? osh["pass"] : 0
|
| 98 |
|
| 99 | } else if (report == "DELTA-osh") {
|
| 100 | bash_total = bash["pass"] - osh["pass"]
|
| 101 | dash_total = dash["pass"] - osh["pass"]
|
| 102 | ash_total = ash["pass"] - osh["pass"]
|
| 103 | zsh_total = zsh["pass"] - osh["pass"]
|
| 104 | mksh_total = mksh["pass"] - osh["pass"]
|
| 105 | ksh_total = ksh["pass"] - osh["pass"]
|
| 106 | toysh_total = toysh["pass"] - osh["pass"]
|
| 107 | sush_total = sush["pass"] - osh["pass"]
|
| 108 | brush_total = brush["pass"] - osh["pass"]
|
| 109 | osh_total = osh["pass"] - osh["pass"]
|
| 110 |
|
| 111 | } else if (report == "DELTA-bash") {
|
| 112 | bash_total = bash["pass"] - bash["pass"]
|
| 113 | dash_total = dash["pass"] - bash["pass"]
|
| 114 | ash_total = ash["pass"] - bash["pass"]
|
| 115 | zsh_total = zsh["pass"] - bash["pass"]
|
| 116 | mksh_total = mksh["pass"] - bash["pass"]
|
| 117 | ksh_total = ksh["pass"] - bash["pass"]
|
| 118 | toysh_total = toysh["pass"] - bash["pass"]
|
| 119 | sush_total = sush["pass"] - bash["pass"]
|
| 120 | brush_total = brush["pass"] - bash["pass"]
|
| 121 | osh_total = osh["pass"] - bash["pass"]
|
| 122 | }
|
| 123 |
|
| 124 | # TODO: change this color
|
| 125 | row_css_class = "cpp-good" # green
|
| 126 |
|
| 127 | row = sprintf("%s %s %s %d %d %d %d %d %d %d %d %d %d",
|
| 128 | row_css_class,
|
| 129 | spec_name, href,
|
| 130 | bash_total,
|
| 131 | dash_total,
|
| 132 | ash_total,
|
| 133 | zsh_total,
|
| 134 | mksh_total,
|
| 135 | ksh_total,
|
| 136 | toysh_total,
|
| 137 | sush_total,
|
| 138 | brush_total,
|
| 139 | osh_total)
|
| 140 |
|
| 141 | # Turn tabs into spaces - awk mutates the row!
|
| 142 | gsub(/ /, "\t", row)
|
| 143 | print row
|
| 144 | }
|
| 145 | ' "${tsv_files[@]}"
|
| 146 | }
|
| 147 |
|
| 148 | summary-tsv() {
|
| 149 | local report=$1
|
| 150 | local spec_subdir=$2
|
| 151 |
|
| 152 | local manifest=_tmp/spec/SUITE-compat.txt
|
| 153 |
|
| 154 | # Can't go at the top level because files might not exist!
|
| 155 | tsv-row \
|
| 156 | 'ROW_CSS_CLASS' 'name' 'name_HREF' "${SH_LABELS[@]}"
|
| 157 |
|
| 158 | # total row rows goes at the TOP, so it's in <thead> and not sorted.
|
| 159 | summary-tsv-row $report $spec_subdir _tmp/spec/$spec_subdir/*.result.tsv
|
| 160 |
|
| 161 | head -n $NUM_SPEC_TASKS $manifest | sort |
|
| 162 | while read spec_name; do
|
| 163 | summary-tsv-row $report $spec_subdir $spec_name
|
| 164 | done
|
| 165 | }
|
| 166 |
|
| 167 | html-summary-header() {
|
| 168 | local report=$1
|
| 169 |
|
| 170 | local prefix=../../..
|
| 171 | spec-html-head $prefix "$report - Shell Compatibility "
|
| 172 |
|
| 173 | table-sort-begin "width50"
|
| 174 |
|
| 175 | cat <<EOF
|
| 176 | <p id="home-link">
|
| 177 | <a href="/">Root</a> |
|
| 178 | <a href="https://oils.pub/">oils.pub</a>
|
| 179 | </p>
|
| 180 |
|
| 181 | <h1>$report - Shell Compatibility</h1>
|
| 182 |
|
| 183 | <p>Back to <a href="TOP.html">TOP.html</a>.
|
| 184 | </p>
|
| 185 | EOF
|
| 186 | }
|
| 187 |
|
| 188 | html-summary-footer() {
|
| 189 | local report=$1
|
| 190 |
|
| 191 | echo "
|
| 192 | <p>Generated by <code>test/spec-compat.sh</code>.
|
| 193 | </p>
|
| 194 |
|
| 195 | <p><a href="$report.tsv">Raw TSV</a>
|
| 196 | </p>
|
| 197 | "
|
| 198 | table-sort-end "$report" # The table name
|
| 199 | }
|
| 200 |
|
| 201 | write-summary-html() {
|
| 202 | local report=$1
|
| 203 | local spec_subdir=$2
|
| 204 |
|
| 205 | local dir=_tmp/spec/$spec_subdir
|
| 206 | local out=$dir/$report.html
|
| 207 |
|
| 208 | summary-tsv $report $spec_subdir >$dir/$report.tsv
|
| 209 |
|
| 210 | # The underscores are stripped when we don't want them to be!
|
| 211 | # Note: we could also put "pretty_heading" in the schema
|
| 212 |
|
| 213 | here-schema-tsv >$dir/$report.schema.tsv <<EOF
|
| 214 | column_name type
|
| 215 | ROW_CSS_CLASS string
|
| 216 | name string
|
| 217 | name_HREF string
|
| 218 | bash integer
|
| 219 | dash integer
|
| 220 | ash integer
|
| 221 | zsh integer
|
| 222 | mksh integer
|
| 223 | ksh integer
|
| 224 | toysh integer
|
| 225 | sush integer
|
| 226 | brush integer
|
| 227 | osh integer
|
| 228 | EOF
|
| 229 |
|
| 230 | { html-summary-header "$report"
|
| 231 | # total row isn't sorted
|
| 232 | tsv2html --thead-offset 1 $dir/$report.tsv
|
| 233 | html-summary-footer "$report"
|
| 234 | } > $out
|
| 235 |
|
| 236 | log "Comparison: file://$REPO_ROOT/$out"
|
| 237 | }
|
| 238 |
|
| 239 | top-html() {
|
| 240 | local base_url='../../../web'
|
| 241 | html-head --title 'Shell Compatibility Reports' \
|
| 242 | "$base_url/base.css"
|
| 243 |
|
| 244 | echo '
|
| 245 | <body class="width35">
|
| 246 | <style>
|
| 247 | code { color: green; }
|
| 248 | </style>
|
| 249 |
|
| 250 | <p id="home-link">
|
| 251 | <a href="/">Root</a> |
|
| 252 | <a href="https://oils.pub/">oils.pub</a>
|
| 253 | </p>
|
| 254 | '
|
| 255 |
|
| 256 | cmark <<'EOF'
|
| 257 | ## Shell Compatibility Reports
|
| 258 |
|
| 259 | These reports are based on [spec tests written for Oils][spec-tests].
|
| 260 |
|
| 261 | Here are some summary tables. **Click** on the column headers to sort:
|
| 262 |
|
| 263 | - [Total Passing](PASSING.html)
|
| 264 | - Each shell gets 1 point for each case we marked passing.
|
| 265 | - Our assertions are usually based on a **survey** of `bash`, `dash`, `mksh`,
|
| 266 | `zsh`, and other shells. Assertions from 2016-2018 may favor OSH, but
|
| 267 | there shouldn't be many of them.
|
| 268 | - [Delta bash](DELTA-bash.html)
|
| 269 | - Compare each shell's passing count vs. bash
|
| 270 | - [Delta OSH](DELTA-osh.html)
|
| 271 | - Compare each shell's passing count vs. OSH
|
| 272 |
|
| 273 | ### Notes and Caveats
|
| 274 |
|
| 275 | - Some tests may fail for innocuous reasons, e.g. printing `'$'` versus `\$`
|
| 276 | - Shell authors are welcome to use our test suite, and add assertions.
|
| 277 | - OSH has some features that bash doesn't have.
|
| 278 | - e.g. I removed `spec/strict-options` so shells aren't penalized for not
|
| 279 | having these features.
|
| 280 | - But I left `spec/errexit-osh` in because I think new shells should provide
|
| 281 | alternatives to the **bugs** in POSIX:
|
| 282 | [YSH Fixes Shell's Error Handling (`errexit`)](https://oils.pub/release/latest/doc/error-handling.html)
|
| 283 | - I also think shells should adopt [Simple Word Evaluation in Unix Shell](https://oils.pub/release/latest/doc/simple-word-eval.html) (i.e. deprecate `$IFS`, more so than `zsh`)
|
| 284 | - Other ideas
|
| 285 | - We could add a "majority agreement" metric, for a more neutral report.
|
| 286 | - We could add the Smoosh test suite. Results are published on our [quality
|
| 287 | page](https://oils.pub/release/latest/quality.html).
|
| 288 |
|
| 289 | ### Shells Compared
|
| 290 |
|
| 291 | - GNU `bash`
|
| 292 | - <https://www.gnu.org/software/bash/>
|
| 293 | - running fixed version built for Oils
|
| 294 | - `mksh` - shell on Android, derivative of `pdksh`
|
| 295 | - <http://www.mirbsd.org/mksh.htm>
|
| 296 | - running fixed version built for Oils
|
| 297 | - AT&T `ksh`
|
| 298 | - <https://github.com/ksh93/ksh>
|
| 299 | - running distro package
|
| 300 | - `toysh`
|
| 301 | - <https://landley.net/toybox/>
|
| 302 | - running tarball release
|
| 303 | - `sush`
|
| 304 | - <https://github.com/shellgei/rusty_bash>
|
| 305 | - running git HEAD
|
| 306 | - `brush`
|
| 307 | - <https://github.com/reubeno/brush>
|
| 308 | - running git HEAD
|
| 309 | - `osh`
|
| 310 | - <https://github.com/oils-for-unix/oils>
|
| 311 | - running git HEAD
|
| 312 |
|
| 313 | TODO: Add other shells, and be more specific about versions.
|
| 314 |
|
| 315 | ### More Comparisons
|
| 316 |
|
| 317 | Possibly TODO
|
| 318 |
|
| 319 | - Binary size
|
| 320 | - Build times
|
| 321 | - Lines of code?
|
| 322 | - [Oils has a "compressed" implementation](https://www.oilshell.org/blog/2024/09/line-counts.html)
|
| 323 | - Memory safety
|
| 324 | - Oils has many other:
|
| 325 | - test suites - `test/gold`, `test/wild`, ...
|
| 326 | - benchmarks - parse time, runtime, ...
|
| 327 |
|
| 328 | ### Links
|
| 329 |
|
| 330 | - Wiki:
|
| 331 | - [Spec Tests][spec-tests]
|
| 332 | - [Contributing](https://github.com/oils-for-unix/oils/wiki/Contributing)
|
| 333 | - Zulip: <https://oilshell.zulipchat.com/>
|
| 334 | - Feel free to send feedback, and ask questions!
|
| 335 |
|
| 336 | [spec-tests]: https://github.com/oils-for-unix/oils/wiki/Spec-Tests
|
| 337 |
|
| 338 | ### Features Not Yet Implemented in OSH
|
| 339 |
|
| 340 | We know about these gaps:
|
| 341 |
|
| 342 | - `kill` builtin, `let` keyword - we do have some spec tests for them
|
| 343 | - `coproc` keyword
|
| 344 |
|
| 345 | EOF
|
| 346 |
|
| 347 | echo '
|
| 348 | </body>
|
| 349 | </html>
|
| 350 | '
|
| 351 |
|
| 352 | # Notes on big files:
|
| 353 | # - spec/strict-options, errexit-osh - could be in the YSH suite
|
| 354 | # - but then that messes up our historical metrics
|
| 355 | # - or we create a new 'spec-compat' suite?
|
| 356 | # - spec/globignore - a big one for OSH
|
| 357 |
|
| 358 | }
|
| 359 |
|
| 360 | write-compare-html() {
|
| 361 | local spec_subdir='compat'
|
| 362 | local dir=_tmp/spec/$spec_subdir
|
| 363 |
|
| 364 | local out=$dir/TOP.html
|
| 365 | top-html >$out
|
| 366 | log "Top-level index: file://$REPO_ROOT/$out"
|
| 367 |
|
| 368 | if test -n "${QUICKLY:-}"; then
|
| 369 | return
|
| 370 | fi
|
| 371 |
|
| 372 | write-summary-html PASSING "$@"
|
| 373 | write-summary-html DELTA-osh "$@"
|
| 374 | write-summary-html DELTA-bash "$@"
|
| 375 | }
|
| 376 |
|
| 377 | # TODO: Publish this script
|
| 378 | multi() { ~/git/tree-tools/bin/multi "$@"; }
|
| 379 |
|
| 380 | deploy() {
|
| 381 | local epoch=${1:-2025-06-19}
|
| 382 |
|
| 383 | local dest=$PWD/../pages/spec-compat/$epoch
|
| 384 |
|
| 385 | local web_dir=$dest/web
|
| 386 | #rm -r -f $web_dir
|
| 387 |
|
| 388 | #mkdir -p $web_dir
|
| 389 |
|
| 390 | find web/ -name '*.js' -o -name '*.css' | multi cp $dest
|
| 391 |
|
| 392 | pushd _tmp
|
| 393 | find spec/compat -name '*.html' -o -name '*.tsv' | multi cp $dest/renamed-tmp
|
| 394 | popd
|
| 395 |
|
| 396 | # Work around Jekyll rule for Github pages
|
| 397 | #mv -v $dest/_tmp $dest/renamed-tmp
|
| 398 |
|
| 399 | tree $dest/
|
| 400 | }
|
| 401 |
|
| 402 | task-five "$@"
|