Side-by-side, interactive cheatsheets for Bash programmers
comparing Bash to other languages. Every example runs live in your browser β no setup, no installation.
Choose your own path by reordering languages
Your pipeline, back inside the language. Ruby's pitch to a shell programmer is specifically not Python's: a block plus Enumerable means fruit.tally.sort_by { β¦ }.each { β¦ } reads exactly the way sort | uniq -c | sort -rn reads β left to right, one stage at a time β except that every stage passes objects instead of text, so nothing is re-parsed and a count is a number you can add to.
grep is select, grep -v is reject, awk '{print $2}' is map, uniq -c is tally, wc -l is count, head is first(n), xargs is each"$variable" you have typed was switching Bash's re-splitting off; there is nothing here to switch off, so a filename with a space stops being a hazardeach, map and File.open are all one mechanism, and why cleanup lives in the library instead of in a trapnil and false are falsy. Zero is true and the empty string is true, which is neither the shell's exit-status convention nor Python's rule β so if value means "is there a value"ruby -ane is a pipeline stage and ruby -i.backup -pe is sed -i, so the migration goes one stage at a time rather than all at oncecase block every mature script grows, with real dependencies that run once however many tasks name themOpen3.capture2 is three lines against one pipe. Move the logic, leave the glueFor the script that has outgrown the shell. Not because shell scripts are bad β orchestration and glue are exactly what a shell is for β but because the moment a script grows a data structure, a decimal point, or a third option, Bash starts charging you for things Python gives away. The clearest single reason: word splitting simply ends. A string is one value everywhere, so a filename with a space in it stops being a hazard you quote your way around.
"$variable" you have ever typed was you switching Bash's re-splitting off; in Python there is nothing to switch off$(( )) is integer-only and anything with a decimal point means shelling out to bc and parsing text back β Python has integers of unlimited size, floats, decimal and fractionsreturn returns a value β not an exit status between 0 and 255 β so a function costs no fork, keeps types, and can hand back a listargparse replaces getopts and generates --help for you, long options includedjson, csv and pathlib replace jq, cut and a pile of parameter expansions β and none of them can be broken by a quote in the dataThis is the migration Perl was built for. It is the language a generation of shell scripts were rewritten into, and the reason is narrow and concrete: regular expressions, hashes and file handling are in the language, so a four-stage grep | sed | sort | awk pipeline β four sub-languages with four quoting regimes running as four processes β collapses into one program with one set of rules.
=~ and s/// return their captures as a list you assign to named variables, instead of a global BASH_REMATCH array indexed by number$count{$_}++ for @items; β the most-typed line in Perl's history, and the whole of sort | uniq -c | sort -rn-e, -f, -d, -s, -r β and substr takes an offset and a length exactly as ${path:5:3} doesreturn returns a value, and a list comes back as a list β no printing it and forking a subshell to catch itperl -lane is a pipeline stage, so you can migrate one gnarly stage at a time rather than rewriting the script in a dayuse v5.38; goes at the top of everything β it is set -euo pipefail, except that a misspelled variable is caught before the script starts rather than when the line is reachedThe shell macOS made the default, Zsh runs almost all of your Bash scripts unchanged β the short list of things that genuinely differ is worth an afternoon, and the parameter flags you get in exchange retire a pile of tr/sort/uniq pipelines.
$variable stays one word, which retires the entire class of bugs caused by forgetting to quote$array means every element, not just ${array[0]}${(j:,:)list}, ${(s:,:)text}, ${(u)list}, ${(on)numbers} β join, split, deduplicate and sort with no external tool and no subprocess*(.om[1]) is the newest regular file β replace most everyday find invocationsalways blocks give the shell a real try/finally, scoped to a block instead of a single global EXIT trapemulate -L sh restores Bash rules for one function, so a ported script can migrate a piece at a timeThe pipe you already know, carrying data instead of text. Nushell keeps | and throws out the parsing β ls | where size > 1mb | sort-by modified needs no awk, no cut, and no counting of whitespace-separated fields.
get size instead of awk '{print $5}', and it cannot drift when the output format changesIFS β a value with a space in it is one value, in every context, with nothing to quotesort, uniq --count, length, first, where are builtins over typed data, so the sort | uniq -c | sort -rn chain collapses to one commandfrom json makes jq unnecessary β JSON, YAML, TOML, CSV and XML all decode into the same records and tablesbc; and 1kb and 1min + 30sec are typed values, not stringstry/catch, replacing set -euo pipefail and the $? dance