Repository navigation
Releases: sf624/bash-explainer
Releases · sf624/bash-explainer
Release list
v0.9.0
Every warning the explanations give is now a problem with a code, a command name that does not run what it suggests gets a hint, code that never runs is faded, and the panel shows how bash reads the line, as the Bash manual does. Scripts can be explained for bash 5.3.
Details and examples: docs/changes/0.9.0.md.
New
- Every warning and caveat the explanations point out (
x = 1,rm -rf $DIR/*,local x=$(cmd)…) is also a problem with a code, in the Problems panel,--check, SARIF and the GitHub Action, also in the shell code of other files. Turn them off withbashExplainer.diagnostics.pitfalls. - New warnings and caveats, among them:
curl … | bash, a variable read while it is unset underset -u, a shebang whose options bash refuses (#!/bin/bash -e -u),find . -exec rm {} ;, atraponINTthat does not exit, an assignment to a read-only variable, andcmd || echo "msg"that turns a failure into success. - A command name that does not run what it suggests gets a hint after it: a function or an alias that hides a builtin or a program, a builtin turned off by
enable -n,sudo cd. Turn the hints off withbashExplainer.hints.names. In the panel, every command name has a badge: keyword, builtin, function, alias or program. - Code that never runs is faded in the editor: a branch whose condition the script has already decided (
if false; then …), the skipped side of&&or||, and what followsexitorreturn. In POSIX mode, a line where bash stops the script is a warning. Turn the fading off withbashExplainer.hints.deadCode. - The new setting
bashExplainer.bashVersion(--bash-version, the Action'sbash-version) explains scripts for bash5.2(the default) or5.3:${ cmd; },${| cmd; }, and what bash 5.3 changes in builtins, variables,printfand[ ]. Compatibility: with the default5.2, a bash 5.3 construct (${ …; },${| …; },shopt -s array_expand_once) is now a warning, and--checkexits 1; setbashExplainer.bashVersionto5.3to explain it as bash 5.3 runs it. - The panel has a Reading view: who reads each part of the line, from bash's own reading to each command and each language inside a word, built up in eight steps.
- Meaning shows the value an expansion has where the script sets it for sure (
msg="hello world"; echo $msg), and the worked examples use it. A brace sequence names its start, end and increment, and$1in a string another shell runs says which word it starts as. - A note that rests on bash's behaviour rather than the Bash manual says so with a chip, with a command to try in your terminal; turn the chips off with
bashExplainer.notes.basis. A new note says that$BASH_SUBSHELLdoes not count the subshell of a pipeline stage. - The new setting
bashExplainer.panel.colorscolours the panel like your editor theme. In a remote window (WSL, SSH, a container), Bash Explainer now runs on your local machine, where the themes are. - Rows in the panel grow and shrink as you fold them, and the row you fold stays where it is on the screen. Turn the motion off with
bashExplainer.panel.animations. - The web page opens and saves files (
Ctrl+O, a drop,Ctrl+S), shows a word's card on hover as VS Code does, and sets the file kind of a pasted script from its content. It opens with its logo and a line of shell read as the explainer reads it. - In bats files, each test is explained as bats runs it (in a process of its own, with
set -e, aftersetup), with the tag comments, functions marked# @test, and the helpers of bats-support, bats-assert and bats-file. - Worked examples of pipelines start from made-up lines in the real layout of
ps aux,ls -l,df -hand others, name the command their input comes from, and mark what the next command reads.
Changed
- Notes say how much they matter: ⚠ Pitfall, ⓘ Caveat or ⓘ Portability, in the editor's warning or info colour. Some squiggles change colour: an unquoted path given to
rmis now a warning, and a Dockerfile pipe without pipefail is now information. - The hover explains the part under the mouse; the whole line opens with
Ctrl+K Ctrl+Eor the hover's Whole line link. The Language Server's hover starts with the part too. - Compatibility: the inline summary at the end of the cursor line was removed, and
bashExplainer.inline.*no longer has an effect. Use the hover,Ctrl+K Ctrl+Eand the name hints instead. - Compatibility:
bashExplainer.highlight.enabledis nowtrueby default, also in the Language Server (before,false). Syntax colouring follows the explanations (aliases, read-only variables, words read as data), and leaves Python files and Dockerfiles to their own language servers. - Compatibility:
--checkprints the new pitfalls but exits as before: their warnings fail it only with the new--strict(the Action'sstrict: 'true'), the Action'swarningsoutput still counts only what fails the step, and its newpitfallsoutput counts the pitfalls. In the JSON output, an alias's tokenkindis nowalias(it wasfunction). - Meaning and Reading show lists as the manual reads them: the commands joined by
;and&make one list. Reading also names and groups each construct as the Bash manual does and cites its section (timeand!start one pipeline), with a row for every reserved word and operator. Every variable of bash's says what it is; before, 85 had no explanation. - The fixes that notes suggest are written for your line, not as a pattern:
local x; x=$(cmd),re='\bfoo'; [[ $x =~ $re ]],for f in "$@","$DIR"/*. - Some explanations read more clearly:
IFS=$'\n\t'names its characters one by one, a brace expansion's words are chips, a special builtin's badge says so, and the shebang says it works only when the file is run as a program. - A new logo, a speech bubble of
[ ]around>_, on the extension, the panel, the web page and the README. The panel's header is a toolbar of icon buttons, as in VS Code's own views, and the web page has the same icons. - On the web page, the address bar no longer shows the script (share links still work), changing the file kind loads its sample (and asks first when the editor holds your own script), the Problems list starts folded on a phone, and the release page's menu no longer has the Version switch.
Fixed
- Words are expanded as bash does in more places: with the shell options and the
IFSin effect (**withoutglobstar), incasepatterns, a tilde after:, sequences past the limits of the bash release, line continuations. In scripts for dash, POSIX sh and busybox ash,{a,b}is plain text, as those shells print it; dash also prints$'…'as text, and in sh its value is not known, and zsh scripts are no longer said to split an unquoted variable. - Tests, redirections and builtins are explained as bash runs them:
[ 08 -eq 8 ]is true,cat <<A <<Breads onlyB,exec >log 2>&1applies to the shell,exit 1 2goes on, andbuiltin cdis the builtin itself. Every option of bash's builtins, and everyshoptoption, is explained. - Meaning gives no value, and decides no condition, where bash may skip a command or the script does what the model does not follow (
shopt -s compat31,set -k, a redirection that fails,[ -v "$1" ]). Before, it could show a wrong value or say that code never runs. - Fewer wrong pitfalls:
echo ===is not taken forx = 1,rm -rf $DIR/*is no warning when the script setDIR, and analiasin a file with no shebang (it may be sourced) or a variable that may come from the environment gets a caveat, not a warning. Each unquoted variable gets its own note, and pitfalls are found undersudo,xargsand a quoted command name too. - Options and operands are read as each tool reads them:
docker run -itis-i -t,tar vf,ps u1,git log --format %H(a value only after=),sed -e … -E,sudo -c. A word past the operands (diff a b c) is an extra operand the command refuses, and find reads its words after quote removal and refuses a path after its expression. - Worked examples follow the line and print as the tools do:
date -d 2024-01-02 +%A,TZ=… date,sort -h,printf '%s\n' {a,b},git log --graph,sed -nwithqorn, and a quoted command name (\sort). - Shell code in other files: a YAML block (
run: |) is read without its indentation, each workflow step, DockerfileRUNand Makefile recipe line is a shell of its own, and notes name the right line. Before, an indentedEOFdid not end a here-document, andset -uin one step counted in the next. - The panel and the web page work with the keyboard and a screen reader: Meaning folds with buttons, a problem opens with Enter, and
F6goes between the editor and the explanation. - In the panel, a long label no longer squeezes its explanation, a click on a row of another line only folds it, and the hover no longer repeats a problem VS Code shows above it. A control character in a value is a box with its name (
LF,TAB). The quick reference lists every command; before, it stopped after 300. - Other languages' semantic colours are no longer changed by ours (a TypeScript parameter was drawn in keyword blue). The web page keeps the phone layout at every zoom, and its tab icon is legible in a dark browser theme.
- The Marketplace, Open VSX and npm pages show the images and links of the version they describe. Before, they showed the newest images from
main, and one was missing. - A long pipeline is explained at once; before, 20 stages took about 30 seconds. Lines being typed (
echo > >g,cat <<) and words such astoStringno longer make the explanation fail.
v0.8.0
Strings that a command runs as a script and bats test files are explained, conditions that the script's own values decide say which code never runs, and worked examples read and print as the tools do.
New
- A string that a command runs as a script is explained as one:
bash -c '…',sh -c,eval, a trap's action,su -c,ssh host '…'andwatch '…', also undersudo,xargs,find -exec,docker execandkubectl exec. Its commands and expansions appear at their own characters, its notes are those of the shell that runs it (ansh -cstring gets the POSIX checks), and the Problems list shows its dialect issues. What a staticevalstring or a trap's action sets counts after it. A string with an expansion (eval "$cmd") says its text is known only when it runs. Before, such a string was one opaque argument. - New notes on conditions that the values the script set decide: "
$modeisfasthere, so this condition is always false: the commands after itsthennever run", or "so onlyfast)of thiscaseruns". Expansions such as${x:-default},${x#prefix}and$(( ))are read as bash expands them. Conditions of literals only (while true,if false) say nothing more. - bats test files (
.bats, or abatsshebang) are explained:@test "…" {,run,load,skipand the other bats commands, with pitfalls for a! cmdthat cannot fail a test and forrunin a pipeline. VS Code, the Language Server, the command-line tool (--kind bats) and the web page (with a bats sample) recognise them. Before,@testwas a syntax error andrunan unknown command. env -Sis read as GNU env reads it. A shebang#!/usr/bin/env -S bash -erunsbash -ewith the script.#!/usr/bin/env bash -ewarns that Linux passesbash -eto env as one word, which fails. Anenv -Sstring is split as GNU env splits it (quotes,\_,#comments,${NAME}), and a string env refuses gets a warning that it exits with status 125.ksh93,mksh,lksh,pdksh,oksh,posh,yashandash(also by path) take the options ofsh:mksh -c '…'runs the string as a script. Before, such a name was an unknown command.- A note says when a command does not read the standard input the line gives it, because it reads the files it names:
jq . a.json < b.jsonreads onlya.json. - The command-line tool takes
--sarif <file>with--check: it also writes the problems to that file as SARIF. The GitHub Action uses it, so it checks the scripts once for both the annotations and the SARIF file.
Changed
- VS Code settings: a setting set below its minimum (
bashExplainer.inline.maxLengthunder 20,bashExplainer.hover.maxDepthorhover.followDelayunder 0) now uses its default, as the command-line tool already did. Before, VS Code used the number as given. - A branch that cannot run, because the script's own values decide its condition, no longer changes how later lines are explained: after
x=1; if [ "$x" = 2 ]; then set -f; fi, a glob is a glob again. Code afterbreak,continue,returnorexitno longer counts either. Before, both sides of every branch counted, so such lines said "may" or "on some paths". In a script that runs code that cannot be read (source,eval "$s",$cmd,${!v}), both sides still count. - The worked examples' sections say what the command reads and where from: "Input:
a.json(sample contents)", "Input: stdin (sample output of the previous command)", "Input: files matching*.json". A section that is not read is no longer called input:date +%Fsays "When it runs (made up, UTC)". Before, every such section was called "Input" (or "Date and time (UTC)"). - The colours of the parts of a line (command, option, variable…) are readable as text: light themes use darker colours of the same hues (at least 4.5:1), high contrast themes their own (7:1), and errors a red that passes too. The web page has the same colours. Before, they were VS Code's chart colours, as low as 2.9:1 on white.
- On the web page, changing the file kind keeps the text, also an untouched sample. "Load the sample" loads the current kind's sample, with an "Undo" button when it replaces your own text. Before, choosing a kind replaced an untouched sample, and "Load the sample" could be undone only with Ctrl+Z.
Fixed
- Worked examples print the output as the tools print it. jq uses two spaces a level (one line with
-c;--tab,--indent nand-Sare followed), and reads-R,inputandinputs. mikefarah/yq prints JSON as yq does, reads a.jsonfile as JSON, prints strings bare with-r, and shows with-iwhat the first file becomes. awk givesFILENAMEthe name of each file.sort -oanduniq in outshow what is written to the file. Before, jq and yq printed every result on one line, andFILENAMEwas empty. - Worked examples read what the command really reads. A here-string, a here-document or a pipe of known text is the input itself (
echo '{"a":1}' | jq .a).jq -nand an awk program ofBEGINrules only show no input. Several files (sort a b,head -n 1 a b) show a section each.catandnawkget real examples. Before, the example made up one input for all. - Worked examples follow the command's options.
grep -e -v foosearches for-v,sed -ie 's/a/b/' -e 's/c/d/' fruns onlys/c/d/, andfind … -exec cmd {} '+'runs the command once. A line the tool refuses (grep -E -F,tar -c -x,seq 1 -w 3) says so and gets no example. A function or alias of the script named like a program (sort() { tac; }) no longer shows that program's output. Before, these examples showed what the tool does not do. - Options are read as each tool reads them. GNU tools (
grep,sed, coreutils,diff,gzip,tar) accept a long option's unique prefix (ls --almostis--almost-all) and know every long option of their--help.testreads no options,xargs --max-lines echorunsecho,perl -0neis-0 -n -e, andbash -O extglob -c '…'andbash --posixare known. Global options are told apart from a subcommand's (docker -H h run,kubectl -o yaml get pods). Before, many of these were "not in the dictionary" or taken for values. - Explanations match bash in more places:
x=1 ''does not setx,exec {fd}>file,wait -p varandcoproc nameset their variables, and a division by 0 in$(( ))discards the rest of the line.unset fremoves the function when there is no variablef.exec cmdin( )ends only the subshell.cmd &> >(tee log)sends both outputs to the process substitution; before, it was read as an argument, or as a syntax error after a block or a loop. In the body offor (( i = 0; … )),$_is no longer said to be empty. [,unsetand declarations are explained as what bash runs, however they are written:\[ -f x ],command [ -f x ],x=1 unset x,\declare a=$v.[ -f xwith no], and wordstestcannot read ([ a b c d ]), say that the command fails with status 2.unset 'a[1]'removes an element, and an unquoteda[1]is a glob first. Where aliases are expanded, an alias named like a builtin runs the alias. Before, these forms were "not in the dictionary" or explained as working, andunsetof a read-only variable said it removed it.- Redirections are read wherever they are written: before the command's name, among its words, and between assignments (
a=1 >f b=2). A command of only redirections and assignments has the status of the last$( )in its redirections, sox=1 >f$(false)fails. A pipeline stage that sends its output to a file (>out grep x in | sort) gets no example of the whole pipeline. Before, a line such asa=1 >f b=2left the whole script unexplained, and such commands had the status 0. - The ⚠ notes follow what bash does here. After
IFS=; set -f,cp $a bno longer says$ais split. A variable set byprintf -vormapfileat the end of a pipeline is reported as lost. The pitfall for a variable another shell does not have sees\$ninbash -c "echo \$n", and knows thatenv -u xremovesx.egrep -qcounts asgrep -q, and an alias ofrmasrm. - The "Idiom" notes are read from the commands, not from the text, so other spacing, order and quoting are found (
while true ; do,done <f,trap cleanup 0). They are no longer given where bash does something else:trap - EXITsets no clean-up,while [ $# > 0 ]writes to a file named0, and text in quotes is not run. - The shell a script runs in is read in more places. In a GitHub Actions workflow,
shell:counts aboverun:, anddefaults:orruns-on:count after the steps. A Dockerfile or MakefileSHELLand a Pythonexecutable=are read as the same shell everywhere:SHELL ["/bin/dash","-c"]is dash. Busybox shells (RUN ["busybox", "sh", "-c", …],# shellcheck shell=busybox) are ash. Before, these were ignored or misread, and the script was explained as bash or sh. - A script is in POSIX mode only when its shebang really starts bash in it (
#!/bin/bash --posix, orenv -Swithbash --posix). Before,--posixanywhere on the shebang line counted, also in#!/usr/bin/env bash --posix, where bash does not start. - With
notes.pitfallsoff, more ⚠ notes are hidden: those for distribution aliases,\bin[[ =~ ]], a DockerfileRUNwithoutpipefail, Pythonsubprocess, GitHub Actions${{ … }}injection and bats files. Before, these ignored the setting. - Quick fixes are offered in every language of the
bashExplainer.languagessetting, as the hover is. Before, they were offered inshellscriptdocuments only. - On the web page's compare view, the Problems list shows the script's problems. Before, it stayed empty and the page raised an error.
- Scripts that assign an element before a function (
n[0]=5 f) are explained; before, explaining them failed. A long chain of functions that call each other in$( )is explained at once; before, 40 of them froze the expla...
v0.7.0
Explanations follow the script as bash runs it and cite the Bash manual; the web page is redesigned for phones and gets the extension's settings.
New
- Explanations cite the GNU Bash Reference Manual (bash 5.2): a "Bash manual 3.7.2" link in VS Code's hover, a "§3.7.2" link in the panel and on the web page, and the section numbers in the command-line tool's text, Markdown and JSON output.
- The shell state is followed in the order the script runs:
setandshoptoptions, variables (also those set by arithmetic, such as(( i++ ))andlet, with their values when known), functions and aliases. A subshell keeps its changes, a function's body counts where it is called, and an option set only in a branch "may" be on. Before, anything earlier in the text counted, even in a subshell or a function that never ran. - New notes on which command really runs: a function that replaces a builtin or a program (and how to reach the other with
builtinorcommand),enable -n,hash -p, special builtins in POSIX mode, names that may come from asourced file, and functions defined in a subshell or removed withunset -f. - New notes on subshells and other shells:
cd,set -eorexitin a subshell only change the subshell, and$$there is still the script's PID. Code run bybash -c,sudo,su -c,sshordocker execgets a pitfall when it uses a variable or function of the script that the new shell does not have, with the fix (export,export -f, or passingdeclare -f). - New
set -enotes:x=$(cmd)stops the script on failure butlocal x=$(cmd)does not; a pipeline withoutpipefailhides a failure; a function ending intest && cmdreturns the test's failure; and the places where bash ignoresset -e. - Idiom notes for
getopts,getoptwitheval set --,while [ $# -gt 0 ],read -d '',find -print0/xargs -0,mapfile,read -p, andwhile read … done < file, whose redirection is now explained on the loop's first line. - Syntax colouring from the same tree as the explanations. The web page uses it always, in the explanation panel's colours, so a word has the same colour in the editor and in the explanation; VS Code and the Language Server send it as semantic tokens with the new setting
bashExplainer.highlight.enabled(off by default). Heredocs end where bash ends them, so the code after one is no longer coloured as a string. - The command-line tool reads a settings file with the keys of VS Code's
settings.json(--config,$BASH_EXPLAINER_CONFIGor~/.config/bash-explainer/settings.json), with--set key=value,--no-configand--print-config. The Language Server takes every setting that applies to it, and reads the same file. Before, only the language, the shell and the examples could be set. - The web page has a settings sheet (the gear) with the extension's settings that apply there, including notes, warnings, kinds of examples and your own commands. Settings can be exported as a
settings.jsonfile for VS Code or the command-line tool, and imported back; "Clear stored data" forgets everything the page keeps in the browser. - On a touch screen, the web page has a read mode: taps move the cursor without opening the keyboard (a tip points to it the first time). A new "Wrap long lines" setting avoids sideways scrolling, and the Problems list can be resized and folded.
Changed
- Your own commands:
bashExplainer.customCommandsnow takes options as a list (each with itsnames) and operands as a list (with"repeat"), plus an optionalstyleandsubcommands. The old shape still works but is deprecated, and a later release drops it. A custom command now follows only what the setting says: one namedheadno longer reads-5as-n 5. - Markdown code blocks tagged
shorshellare read as bash unless they start with a shebang, so they no longer get POSIX sh warnings. To check a block as POSIX sh, start it with#!/bin/sh. - The web page is redesigned: it fits the window, with the editor and the explanation in panes and a draggable divider between them. A compact top bar replaces the toolbar and footer; a menu slides in from the left and the settings from the right (also with swipes), and slide out again when closed with ×, Esc or a tap beside them. The explanation panel's header moved to the page: "Collapse all", "Quick reference" and "Copy as Markdown" are buttons in the top bar, and the shell and yq are in the menu (and the top bar on a wide window). The file kind is a compact button with a list in the top bar. On a phone the top bar shows the logo and the name "Bash Explainer", and the file kind, the copy and the link are in the menu; on a touch screen the quick reference no longer opens the on-screen keyboard (VS Code's panel is unchanged).
- The web page remembers the language, shell and yq you chose. Before, they were reset on each visit.
- In the panel, the current line stays at the top while the explanation scrolls. Clicking a row of an
ifor a loop no longer moves the cursor to its last line, and clicking a row scrolls a long line sideways to that part.
Fixed
- Explanations match bash in more places:
~after=and:in assignments and assignment-like arguments,nullglob/failglob/GLOBIGNORE,set -fon some paths, ambiguous redirection targets (> *.log), no globbing inx=*.txt, no word splitting in<<< $xor${s:$i}, unquoted patterns incaseand[[ ]],a=(x)y, and$_. - Words a command passes to another command are read as that command reads them:
ssh host ls -l,flock f -n true,bash -ex script.sh -x,sh -s -- --prefix x,env -S,sudo -i cmdandwatch cmd. Options written with quotes or expansions (ls "-l",ls -$opts) are options; abbreviated long options are read when the tool accepts them, and ambiguous ones are shown as such. - Options of specific tools are read as the tools read them, among them
git's global options,ip's abbreviations,ps,kill,trap,declare,pushd,exit -1,command -v,timeout,locate -r,arand/bin/echoor other builtins run by path. - Editing an example's input in the VS Code panel updates the output again when the command has a variable (such as
"$f"). Thexargs -0 -I{}example runs once per name, andjq 'def g(x'no longer stops the explanation. - A Dockerfile
RUNwarns about a pipe withoutpipefailunlessset -o pipefailruns before the pipe; before, the wordpipefailanywhere in theRUN, even in a comment, silenced it. - On the web page, the Problems list shows
codeas code instead of raw backticks.
v0.6.0
- The command a wrapper runs is found correctly, and the pitfalls of wrappers are pointed out.
bash -c 'echo $1' a b(alsosh,dash,zsh,ksh, and-xc,-o pipefail -c) saysabecomes$0andbis$1;bash script.sh a bcallsaandbarguments$1and$2.kubectl exec|run|debug … -- ls -la,docker run|exec image cmd,docker compose run|exec svc cmd,podman,chroot dir cmd,flock file cmd,uv run,poetry runanddoasexplainls/cmdas the command that runs, with its own arguments.timeout 5 sleep 10takes5as the time limit andsleep 10as the command;env A=1 cmd,sudo A=1 cmdanddoas A=1 cmdtakeA=1as an assignment;nice -10 makereads-10as the adjustment-n 10;su - userreads-as the login option.sudo cd /rootandtimeout 5 cd /tmpare both explained ascd. New notes: a wrapper (sudo, doas, env, nohup, timeout, nice, xargs, find -exec, chroot, flock, setsid, watch, strace, stdbuf, ionice and others) starts a separate program, so a function of the script or a builtin such ascd,export,sourceorreadcannot run (with the fix:export -fandbash -c, orsudo sh -c '…'; also for a function named afterssh hostorsu -c);env FOO=1 echo $FOOandFOO=1 echo $FOOexpand$FOObefore the variable is set;ssh host ls $HOME/*.logis expanded by the local shell;{ echo a; echo b }has no;or newline before}, and{echo a;}has no space after{. Before,fooofbash -c '…' foowas "the script to run",lsofkubectl exec pod -- lswas "Pod", the5oftimeout 5 cmdand theA=1ofenv A=1 cmdwere taken as the command (andcmdas its argument),nice -10as the options-1 -0,sudo cdandtimeout 5 cdwere explained differently, and none of these mistakes was pointed out. - Builtin options are complete and read correctly. Signals of
kill(-TERM,-SIGTERM,-15) are one option,kill -l 15andtrap -p INTtake signals,kill -n 9,wait -p v,read -N 3 / -i text,exec -a name cmd, and all value options ofmapfile/readarraytake their values,getoptsnames the words to parse,. file argsthe arguments, andtrap INTresets the signal.%1,%%and%nameare job specs.set -o/set +oalone list the settings.export -nremoves the export property (it said "name reference"),exportandreadonlyhave their own options,declare +xturns an attribute off,unset -nis an option,fc,bindandsuspendare builtins,jobs -l,disown -h,type -p,hash -r,declare -tand more are known.[[ a != pat ]]says the right side is a pattern, and the example of[ -n $x ]no longer calls an empty value false. Before, these were read as combined short options, as PIDs or variable names, or as unknown options. - Declarations, aliases, special builtins,
>&andtestare explained correctly.LC_ALL=C export XandV=1 declare -x Ystay builtins (they were "external commandexport"); with-for-Fthe names are functions (export -f fn); every declaration builtin andunsetcarries "(shell builtin)";cmd >& filesays stdout and stderr; the example of[ $# -eq 1 ]says "1 argument" (it was#="1"); an alias in a bash script withoutshopt -s expand_aliasesis "command not found" (it said "replaced by …"); under#!/bin/shorset -o posix, an assignment before a special builtin (V=1 :) stays after it (it said "only for this command"); the words oftestare read as an expression like[ ](test "$a" = "$b" -a -f x,test ! -e f; they were all "Word"). - Shell-language explanations corrected after an audit against bash 5.2.
"$@","${a[@]}","${!pre@}"and"${@:2}"are "one word per element" (before: "stays one argument"). The examples of subscripted expansions use an array element (${#a[1]}witha[1]=hellois 5; before, the scalara=hello, which bash reads as element 0 only).${1:=x}and${@:=x}say that bash fails with "cannot assign in this way" (before: "assign to$1"), and${@:-none}is worded with arguments (before:@=hello). The home directory is expanded in the word of an unquoted${x-~/d}.${!a[@]:-x}and${!v:-x}are indirection followed by the operator (before: the key list, or the operator was left out).shopt -s globstar; ls **/*.txton one line has globstar on (before: a pitfall note), and so on fordotglobandnocaseglob.unset IFSand a value that is not known (IFS=$old) end the earlier IFS note;set -fends "globbed". The end line of a<<-here-document is called its end, and the operator<<-END(before: body,<<END). A{ }group in a pipeline, after&or in acoprocis said to run in a subshell.[]a]is a bracket expression, not "no wildcards".: ${V:=x}assigns also whenVis empty. - New notes:
((i++))withiat 0 has status 1 (set -estops);exit 256/return 300are taken modulo 256;localoutside a function fails. A missing;or newline beforethenordo(if [ -f a ] then …,for x in a b do …) is reported as a syntax error; before,then/dolooked like an argument and nothing was said. - Options and operands of external commands are explained correctly in more places:
kubectl logs -f POD(-fis--follow, not--filename),git tag -a -m MSG TAG(-atakes no value),rpm -ivh(-iis install;-qiis info),chmod/chown/chgrp --reference=RFILE FILE(every operand is a file),fdisk -o(output columns;-w|--wipeis the wipe option) andfdisk -u -l(-utakes a value only with=),systemctl -r,date -Iseconds(the precision is the value of-I, not-s),journalctl -b -1(the boot offset or ID),seq -f FORMAT 1 10, andpip uninstall/show(a package name, not a pin). New: roles of the operands ofuniq,zip,unzip,mktempandpgrep/pkill, andunzip -l,diff -c -N -w -i -B,mktemp -p --suffix -u -q,xargs -i. Before, these were shown with the wrong meaning, the next word was taken as a value, or the words were left as "Word". - An option with its value glued on and partly quoted is read as the option and its value:
awk -F'|',cut -d',',sed -e's/a/b/',grep -e'x',tar -C'/tmp',awk -F' 'andawk -F"\t"(the value reads ",after quote removal", and a-e/-Fvalue in another language is broken down). Before, the whole word was a stray argument, so the next word (the awk program, or a file) took its place and the example was wrong or empty. - jq options are complete and read right:
--arg,--argjson,--slurpfileand--rawfiletake a name and a value (also in kislyuk/yq),--indent N,-f FILE(then the first operand is an input file),--argsand--jsonargs(the operands are strings or JSON values, not files),--tab,-S,-a,-j,--raw-output0,--seq,--stream,-C,-Mand-L. The example follows-s(the file holds the values, one per line, and the filter reads them as one array:jq -s lengthgives their number) and-j(no newlines). Before,jq --arg n foo '.name = $n' in.jsontookfooas the filter, and-sand-jwere ignored. - jq: destructuring (
. as [$a, $b],{k: $v},{$a},{(expr): $v},?//, inas,reduceandforeach) andlabel $x | … break $xare explained and run in the examples; the sample input fits.[] | @csv/@tsv,contains(…),inside(…),index(",")and. + [..]/. - [..]/. + {…};test(re; "x")ignores whitespace and comments in the pattern;index/indiceson an object give null (and fail on a number or a boolean);fromjsongives jq's message for a non-string. Before, these filters were "Could not parse", the sample input made.[] | @csvandcontains("a")fail, andtest("v a"; "x")was wrong. - In
[[ $x =~ \bfoo ]]the unquoted backslash before an ordinary character is removed by bash, so\b,\w,\s,\dand\<are the plain charactersb,w,s,dand<; the regex is explained so, with a warning that says to keep the regex in a variable (re='\bfoo'; [[ $x =~ $re ]]) or to use[[:space:]]. Before, they were explained as the GNU word boundary and classes, which bash never passes to the regex. - The sed examples and explanations follow GNU sed more closely. An empty match right after a match is skipped, as GNU sed does:
s/x*/-/gonx xxgives- -(before,-- --),s/b*/X/2onb bbgivesb X;0,/re/can end on line 1 (before,0read as "line 0" and the example ignored it);sed -zshows no example (before, it was run on lines);s/o/0/2gsays "every match from the 2nd on" (before, "every match"); an empty pattern (s//x/) says "the last regex"; an incomplete\{ingrep 'a\{1'is an error (before, a literal brace). - awk is explained and run more correctly. A comma inside a call in a pattern, as in
match($0, /re/) { … }, no longer makes a "range pattern";name=valueoperands (awk '{print $1,$2}' OFS=, f) are described as assignments and used by the example (before, "target file"), and escapes in-vand-Fvalues are processed (-v OFS='\t'is a tab,-F '\\|'splits at a bar);$0 = …says that the fields are split again,delete athat the whole array is emptied, andRS = ""that it is paragraph mode. An example that needs anotherRS, aCONVFMT, the 4th argument ofsplitor moreprintfformats than values is not shown, as it could not match awk. printfin bash and awk examples matches C:%#xand%#o(0xff,010), a negative number in%x,%oand%u(two's complement,ffffffffffffffff),%.0dof 0, and%.3eof 12345 (1.234e+04, ties go to the even digit as in glibc; before,1.235e+4). bash'sprintf %d 1.5and%d 1e3("invalid number") no longer get an example.findis explained as it behaves.-mtime +7is "at least 8 days ago" (days are whole, the fraction is dropped;-mtime 7is from 7 to 8 days,0the last 24 hours);-sizeis ...
v0.5.0
- The parser is now unbash, a small JavaScript parser, in the extension, the web page, the command-line tool and the Language Server. Before, it was tree-sitter-bash running as WebAssembly. The
.vsixis a quarter smaller (703 KB, was 947 KB) and the web page downloads 1.5 MB less (no.wasmfiles); a large script is parsed about three times faster; nothing is loaded at start-up. Scripts that the old grammar misread are read as bash reads them:cat <<EOF; cmd(the delimiter isEOF, notEOF;), a pipeline followed by a later&&line (tree-sitter-bash#347),for i do,<<-with tabs,coproc,[ ]with escaped parentheses ([ \( a = b \) -o c = d ]is grouped),[ ! = a ], and a here string after}. An unfinished script ({ echo,case $x in,echo "abc) is explained with the parts it has, not as a syntax error. - About 400 more commands are explained, with their frequent options, instead of "not in the dictionary": the ones named in the LPIC-1/2 and LinuC-1/2 objectives (
fdisk,parted,mdadm,pvcreate/lvextend,grub-install,rpm,zypper,chage,at,hwclock,nmcli,tcpdump,nmap,rndc,smbclient,iptablesoptions,firewall-cmd,semanage,iostat,sar,virsh,ansible-playbook…), basic ones (man,locate,whatis,cal,w,last,shutdown,reboot,init,traceroute,tree,chroot,tmux,/usr/bin/time…) and the common macOS tools (open,pbcopy,say,caffeinate,diskutil,launchctl,networksetup,mdfind,xattr,codesign,plutil,sips,system_profiler,dscl,lipo…). Options that only some implementations have say which (nc -eis not in OpenBSD netcat,shutdown -tand-Fare ignored by systemd,mail -aattaches a file in s-nail but sets a header in bsd-mailx), and options that do nothing are described as such (chronyc -a,lp -c,more -u). Older commands also know many more options (ls,cp,tar,ps,sort,mount,useradd,ssh,curl,rsync,ulimit…) and subcommands (systemctl list-units,dnf history,ip netns…).systemctlandipexplain their global options after the subcommand too (systemctl status -l x); before, only the options before it were known./usr/bin/time lsis explained as the externaltime. - Options written without a dash in the first argument are explained letter by letter:
tar cvzf a.tgz dir,ps auxandps axjf(BSD options, whose letters mean something else than-a -x -u),ar rcs lib.a x.o,jar cvf,dump 0ufandrestore rf. Letters that take a value (f,C,o…) use the following words in order (tar cfC a.tar dir). Only a first argument made of known letters is read this way (tar --create,ps 1234andps -efare not); before, it was "Wordcvzf".ar,jar,dumpandrestoreare in the dictionary, andtarknows more options (-r,-u,-A,-d,-p,-k,-m,-O,-h,-P,-a,-b,-T,-X). - A command with redirections is now a
redirecttoken that holds the command and its redirections side by side, as{ …; } > falready was: fortr -d '\r' < windows.txt > unix.txt, the description and the worked example are ontr -d '\r', and< windows.txtand> unix.txtare separate tokens next to it. This holds when the redirections are all before or all after the command's words (>f echo x,<<<'1 2' read -r a b); with one in the middle (echo a > f b) the command is still one token. The summary line is unchanged. Before, the redirections were children of the command, and its description covered them. - A script with Windows line endings (CRLF) is explained as if it had LF, and a warning (
crlf, on the last word of the first such line) says that bash reads the CR as part of the last word of each line (fibecomesfi\r), so the script fails. The warning underlines the last word of the line, and# bash-explainer disable-file=crlfanywhere in the file turns it off. Before, the extension said nothing, and the unbash path readfias a command. - An alias is used only on the lines read after the
aliasran, as in bash:alias e='echo A'; eon one line,ebefore thealias, and a function or{ }body read before it are explained as the commande(with a note), not as the alias;eval ereads the string again, so it can use it. A lateraliasof the same name replaces the earlier one. Before, an alias applied to the whole file, and the first definition always won. set -eou pipefailandset -oeu pipefailare explained as bash reads them: anoanywhere in a bundle takes the next word as the option name (pipefail), and the other letters (u,e) still apply; the same forset +eo pipefail. Before,-eoulost theuand-oeuwas explained as-oalone. The "strict mode" note is shown whenever onesetcommand turns on errexit, nounset and pipefail, in any order and form (-eou pipefail,-o pipefail -eu,-e -u -o pipefail,-o errexit -o nounset -o pipefail); before, only-…e…u…o pipefailwas matched.${x@Z}and${#a-b}are explained as a bad substitution, as bash reports them;${x@Z}made the explanation fail.${(f)var}is explained as a bad substitution too, as bash reports it (with the shell set to zsh, it is marked as zsh syntax that is not parsed, as before). A bareecho (is still accepted without an error, where bash reports a syntax error.declare a[1]anddeclare a[@]explain the element or all elements of the array.- Words that start with a digit in arithmetic (
0b11,1e3,08) are flagged as numbers bash rejects, and an empty$()in a command name ($()echo hi) is removed, with the unbash parser too. The base-64 digit@in arithmetic is a limit of unbash:$((64#@))alone is read as 62, but next to other terms ($((64#@ + 1))) the whole expression becomes one word. printf $"Copyright %s Inc." 2024is explained like the same format in plain double quotes: the breakdown of the format and the example are shown. Before, the format of a$"…"string was not looked into.- On the right of
==,=and!=in[[ ]], any glob or extended glob gets a pattern token:[[ $x == *.txt ]],[[ $x == --?(a)b ]]and[[ $x == -@(!(-*)e*|-edit) ]]. Before, only some patterns had one, and the extglob that did not start the right side had none. - A label that ends in a blank, such as
' 'intr -d ' 'or"PV ", no longer gets a " …" at its end; that is only added when the text goes on in a later line. - A pipeline that starts with
printf '\231' | …(a raw byte) is explained without the example, instead of failing withUnsupported: raw byte. Other formats that the example cannot show (\xwithout digits,%qwith control characters…) too. - A
casepattern list that goes on after a backslash (a|b|\thenc)): the later line shows the patternc. Before, its|was explained as a pipe, the)as the end of a subshell and the pattern as a word. - An array element with spaces in its subscript,
a=([1 + 1]=x)ordeclare -A m=([a b]=1), is explained as one element with an index (2) or a key (a b), as bash reads it up to the]. Before, the tree-sitter path split it into three words ([1,+,1]=x) and said three elements.[1 + 1] =x,[1 + 1] = xandx[1 + 1]=xare still separate words, as in bash. ${a[@]@K}and${a[@]@k}say that an indexed array gives index and value pairs too, not only an associative one. The Japanese for@E,@A,@K,@aand@kis smoother (it had 「値を…を…する」).[ 1+1 -eq 2 ],[ x -eq 2 ]and[ 1.5 -eq 1 ]say that[ ]reads only integers for-eqand the like, so the operand is an error (integer expression expected), and[[ ]]would calculate it.[[ 1+1 -eq 2 ]]says that it is read as arithmetic (2). Before, they said "numbers are equal" only.- An array element whose key is quoted, such as
["a b"]=1,[']']=3or['q r']=zinh=( … ), is explained as an element with a key and a value, like[c]=2; the key is shown without its quotes (a b). Before, it was a "concatenation" of[, the string and]=1. The closing]is the first one outside quotes, expansions and nested[ ], as in bash, so['a=b']=cand[a\]]=cwork too. ${!prefix@}and${!prefix*}are explained as the list of the names of the variables that start with the prefix (@: separate words,*: one word inside double quotes), with the@and*described as such. Before, they were an "indirection" through a variable named like the prefix, the@was called a transform, and${!zq_*}made the explanation fail.${!x@Q}says indirection first, then the transform.=and==in[ ]andtestare explained as comparing plain strings ([ a = a* ]is false). The note that the right side is a glob pattern is shown in[[ ]]only; before,[ ]had it too.- Extended globs (
+(a),@(a|x),*(b),?(a),!(a)) in${s#…},${s//…/…}and the like are explained as extended globs that needshopt -s extglob. Before, they said "contains expansions, so it is decided at run time"; that is said now only when the pattern has a$…or a backquote. - An unclosed backquote,
$(or$((in the pattern of${s^^…},${s#…},${s:-…}and the like, followed by a backslash at the end of the text, no longer makes the unbash path throw. Before, unbash ended such a part past the text, and the explanation failed. - A backquote substitution inside double quotes is read as bash reads it: the backslash before
$, a backquote,\and"is removed before the command is parsed, soecho "`echo \"x\" y`"shows the stringx(the unbash path mapped the positions, but the text still had the stray backslash).\\\\in a backquote is now\(one backslash), not\\, in both parsers. Before, the explanation of such words removed only the quotes. [[ x =~ "$re" ]],=~ 'a.c'and=~ a"."csay that the quoted ...
v0.4.0
- The web page's shell sample is a tour of every feature, in sections: basics, commands with worked examples, the pitfalls pointed out, and tricky syntax; it was a short script of 15 lines.
-aand-oin[ ]are grouped as bash does:[ "$a" -eq 1 -a "$b" -eq 2 ]is two comparisons joined by AND,[ ! a = b -a -f c ]is!(a = b)AND-f c, and[ -a file ]and[ -o noclobber ]are the unary tests; they were mis-grouped (("$a" -eq 1 -a "$b") -eq 2,-f (a -a (-f b)),!(a = b -a -f c)), and-a filewas an "arithmetic unary operator".-g,-u,-k,-O,-G,-Nand-Rare explained as tests too.${a[@]:1:2}and${@:2}are explained as taking elements (of the array, or the arguments from$2), with examples checked in bash; they were explained, with a wrong example, as taking characters.- An assignment with only a redirection (
FOO=1 2>/dev/null) is explained as an assignment in the shell; it was "the external command ``". - perl one-liners that leave the
-n/-ploop withlast(perl -ne 'last if /^END/; print') get worked examples, as perl runs them: the loop stops,ENDstill runs. - jq filters with
.a.[0](jq 1.7) or adefafter a comma (1, def f: 2; f) are explained; before, the whole filter was "could not parse". git log --date=relativeexamples over a year old read like git's: "1 year, 1 month ago" up to five years, instead of "1 year ago".- perl one-liners with a
BEGIN { … }orEND { … }block followed by more code without;(perl -lne 'END { print $n } $n++') are explained statement by statement; the block and the code after it were taken for one assignment. exec 3>&-andexec 3<&-are explained again as closing the descriptor; since the parser update in 0.3.0 they were only "redirection>&-".- A Jenkinsfile
shstep starting with a shebang (#!/bin/bash,#!/usr/bin/env zsh) is read as that shell; the shebang was misread before. **in file-name globs is explained as any depth of directories, as it is meant, with examples such assrc/lib/error.ts. When noshopt -s globstarcomes before it, a pitfall says that bash then treats**as*(one level only), unless a file read withsourceturns it on; aftershopt -u globstar, and in shells without globstar (sh,dash…), it is explained as*. zsh always has it. Examples no longer show names likeappapp/app.ts. The options ofshopt(globstar,nullglob,dotglob,extglob…) are explained.- Glob examples follow
shopt -s dotglob(names starting with.match too),nocaseglob(file names ignore case) andnocasematch(caseand[[ == ]]patterns ignore case), set earlier in the script; the explanation says which is on. - Extended globs (
!(*.txt),*.@(jpg|png),+([0-9])…) in arguments are read as one word; before, the parser took!(…)for a subshell and explained*.txtas a command. They are explained part by part, with matching and non-matching names. A pitfall points out a missingshopt -s extglob(bash's syntax error), or one inside the same function or block as the glob, which comes too late because bash reads the whole block first. In zsh,*(…)is noted as zsh-only syntax (glob qualifiers), as before. - A pitfall for
for f in *.log(and"$dir"/*): when nothing matches, the loop runs once with the pattern itself. It suggests[ -e "$f" ] || continueorshopt -s nullglob, and stays quiet when the body already checks, or with nullglob or failglob on. - A pitfall for variables set on the right of a pipe and read after it (
cat f | while read -r l; do n=…; done; echo "$n"): the pipeline ran in a subshell. It suggestsdone < file,< <(command)orshopt -s lastpipe, and stays quiet in zsh and ksh, where the last part runs in the shell itself. - Aliases defined in the script (
alias ll='ls -l') are explained where they are used, with a pitfall that bash does not expand them in scripts withoutshopt -s expand_aliases. - Variables of CI systems are explained: about 100 environment variables that GitHub Actions, GitLab CI/CD, CircleCI, Jenkins, Bitbucket Pipelines and Azure Pipelines set (
$GITHUB_WORKSPACE,$CI_COMMIT_SHA,$BUILD_NUMBER…), unless the script sets them itself; the contexts of GitHub Actions expressions (${{ github.head_ref }},secrets.*,matrix.*,steps.<id>.outputs.*…); Azure Pipelines macros ($(Build.SourcesDirectory)); and Jenkins'${env.BRANCH_NAME}and${params.X}. - Fixes found by new tests:
$"…"(a translated string) is explained as such instead of as a plain string without its$;unsetexplains its options and names; a Pythonshell=Truescript with\x41or\u0041escapes is decoded as Python does; perl one-liners withelsifget worked examples. - The pitfall for
rm -rf "$DIR"/…no longer fires on special parameters such as"$$/x", where it suggested${VAR:?}. git logexamples with%<(1,trunc)show.., as git does, instead of the first character; a width of 0, which git does not take, gets no example.- perl one-liners:
s{a{1}}{b}(nested brackets) ands{a} {b}are split into pattern and replacement as perl does, ands/a//reads "delete" instead of "replace with ``". - awk: the pattern
0is explained as always false; it was "always true (non-zero)". - Regex examples:
a{,2}ingrep -E/sed -E(anda\{,2\}in basic regexes) is read as "at most 2", as GNU tools do, instead of as the text{,2}; ingrep -P,[]a]is a class of]andainstead of a class that matches nothing. printf '%c' ""in examples prints a NUL byte, as bash does.- Extended globs such as
!(app)*, where!(…)is followed by a part of varying length, get no examples: bash may end!(…)at any point, so names likeappappmatch, which the examples said did not. - mikefarah/yq v3 paths:
(.==x)is explained as the elements equal tox, instead of the elements whose.matchesx. - sed examples follow GNU sed in more cases:
\L/\Ucancel a pending\u/\l(s/.*/\u\L&/is all lower case); an empty regex (//) is the last one used when the line runs, not the one written before it; and scripts GNU sed rejects (\9for a group the pattern lacks,//with no regex used yet) get no example instead of a made-up output. - Shell dialects:
selectloops andread -n1(a value attached to-n,-t…) are noted in scripts for shells that lack them; the parser readsselectas aforloop, and only separate values were seen. - Examples for
${v#[[:digit:]]}and other patterns with POSIX classes ([[:alpha:]],[[:space:]]…) follow bash; the class was cut at its first]. - coreutils examples:
sort -hputs0Kwith the zeros, as GNU sort does, andtr a ''gets no example (GNU tr rejects an empty second set). - Pipeline and awk examples:
grep -o -vprints nothing, as GNU grep does, instead of the non-matching lines;awk -Ftsplits on the lettert, as gawk and mawk do, not on tabs (-F'\t'is a tab). - Host files: a GitHub Actions step with
shell: pwsh(or anyshell:of its own) is no longer read with theshell:of the step before it; a Dockerfile exec-form string with\nno longer shifts the lines after it; a Python call with comments between its arguments (run( # why) is found. - awk examples follow gawk and mawk in more cases:
substr()truncates its numbers and keeps the length when the start is before 1 (substr("hello", 0, 2)ishe);\\in asub()/gsub()replacement stays two backslashes unless&follows; programs gawk rejects (an array used as a value,(a, b)outsidein, a newline right after(, a built-in without its argument) get no example. - perl examples follow perl in more cases:
!andnotgave the opposite truth value;reverse 1..3reverses the list;split " "andsplitalone split on blanks (the latter crashed); a match in list context gives its groups (my @m = /(\w+) (\d+)/);tr/a/b/ssqueezes only translated characters;nextinside aforloop goes on with the next item; with-F, fields are split with the newline, as perl does;"\Lab\uc"isabc. Code perl rejects or that depends on unknown values (sort { … }with nothing to sort,"@a[0]","a@b.c") gets no example. - jq examples follow jq in more cases:
roundtakes halves away from zero (-2.5→-3);trykeeps the values output before an error; an error on the left of//is reported, not replaced by the default;@base64dfails on text it cannot decode; assigning to a negative index out of range (.[-5] |= 1) is an error;$__loc__names<top-level>. Filters jq rejects or that differ between jq releases ({a: .b | f},{(…)},ascii,|= empty) get no example. timeis explained as bash's keyword that times the command after it (-pfor the POSIX format), and that command is explained as usual; it was an unknown external command.- About 70 more commands that distributions install by default:
flock,journalctl,ping,dig,lsof,free,top,crontab,setfacl,unshare,nsenter,mkfs.ext4,ssh-add,shuf,vim, … (with their frequent options). [[ -f a && $x =~ re ]],[[ -f a && $x = y ]]and[[ $x = *.txt || $x = *.sh ]](=and=~next to&&and||,=with a glob on the right),[[ $x = *"$y"*z ]]and[[ $x = ~/a* ]](a pattern of several parts, or starting with~, after=,==and!=),[[ ! $x =~ re ]],[[ ! $x = y ]],[[ ! $a -eq 1 ]],[[ ! $a < b ]]and[ ! a = b ](!negates the whole test), and${s:$i},${s:$i+1}and${s:$i-1:1}are read as bash does; the parser's grammar is patched so that=and=~in[[ ]]bind tighter than&&and||,=takes a pattern like==does (with any number of quoted and expanded parts in it),!binds looser than the comparison after it, and a variable can be the offset or length of a substring, with or without arithmetic after it. Before,[[ -f a && $x =~ re ]]was read as `(-f a...
v0.3.0
- About 100 more commands and builtins, chosen from a survey of shell scripts, Dockerfiles and workflows in popular repositories: package managers (
apt-get,apk,dpkg,dnf,brew,pip), users and groups (id,useradd,adduser), system information and services (uname,nproc,systemctl), signatures and checksums (gpg,openssl,sha512sum), compression (xz,zstd), toolchains (go,cargo,node),gh,shellcheckand more, plus frequent options of known ones (set -E,test -z,tar --strip-components,curl --retry,jq --argjson…). In that survey, the share of commands explained went from 80% to 89%. - About 45 more from a second survey, of operations and cloud scripts, node bootstrap scripts and dotfiles:
kubectl,helm,kustomize,kind,terraform,aws,gcloud,az,cosign,podman(with docker's subcommands),envsubst,dd,lsblk,iptables,usermod,ssh-keygen,bc,npx,pnpm, macOS'sdefaultsandhdiutil, and more;docker buildx/system/inspect,gh proptions,git submodule/symbolic-ref/show-ref,comm -1 -2 -3. Across both surveys, about 90% of the commands met are now explained. terraform -var-file=xandgo test -count=1: single-dash long options with=valueare read.head -1,tail -5andgit log -3are read as-n N.sed -i.baktakes.bakas the backup suffix instead of more options.gawk,mawkandnawkare explained asawk.date +FORMATgets a worked example: what it prints at two fixed moments (in UTC), editable in the panel. It replaces the note "right now this prints…", which changed with the time, so the same line now always gets the same explanation. More conversions are worked out (%s,%I,%p,%Z,%z, week numbers,%c…), checked against GNUdate. Turn it off with thedatekind inbashExplainer.examples.disabled.- Pitfalls are pointed out where they appear, with how to avoid them:
x = 1(a command, not an assignment),rm -rf $DIR/…whenDIRmay be empty, unquoted variables as paths and inside[ ],for f in $(ls)/$(find),for a in $*,readwithout-r,local x=$(cmd)hiding a failure, andsudo cmd > /etc/…. In about 165,000 lines of the surveyed scripts they were found about 400 times. Turn them off withbashExplainer.notes.pitfalls. - More CI systems: Jenkinsfile
shsteps (in every quoting, with the$nameand${…}that Groovy fills in pointed out), CircleCIrun:steps written withname:andcommand:, and Azure Pipelinesscript:andbash:steps, whose$(variable)macros are no longer taken for command substitution (script:on Windows agents, and PowerShell steps, are left out). The web version and the command-line tool (--kind jenkinsfile) read Jenkinsfiles too. - Code that is not shell is no longer explained as shell: the JavaScript of
actions/github-script(with: script:),run:steps of jobs on Windows runners (PowerShell by default, unless the step saysshell: bash), and Dockerfile stages that use PowerShell or cmd (SHELL ["powershell", …], Windows base images).SHELLnow applies until the nextFROM, as in Docker. - A step's
shell:written after- run:is now taken into account. - The parser is updated: tree-sitter-bash 0.25.1 (from 0.23.3) with web-tree-sitter 0.27.0 (from 0.22.6). Explanations of bash scripts stay the same; scripts with syntax the grammar cannot read, such as zsh-only code, may be split up differently.
v0.2.0
- A command-line tool, the npm package
bash-explainer:npx bash-explainer deploy.sh:12explains lines in the terminal (text, Markdown or JSON), and--checkreports what the script's shell lacks, as compilers print warnings. - A Language Server for other editors:
bash-explainer --lspgives Neovim, Helix, Emacs and others the same hovers, warnings and quick fixes (see the CLI's README for the set-up). - Checks in CI:
bash-explainer --check --format github(annotations on the pull request) or--format sarif(code scanning), and the repository as a GitHub Action,uses: sf624/bash-explainer@v0. - Works in VS Code for the Web: on vscode.dev and github.dev.
- The web version now links to the licenses of the software it includes (CodeMirror and others), listed in THIRD_PARTY_NOTICES.md.
v0.1.1
- A web version to try Bash Explainer without installing it: paste a script and see the explanations, worked examples and warnings in the browser, for shell scripts and the other files the extension reads. It runs in the browser alone, like the extension, and can share a script as a link.
- The extension itself behaves as in 0.1.0. It is now tested in VS Code 1.85 (the oldest it supports) and the latest, on Linux and Windows, with checks that it opens no connection and starts no process.
v0.1.0
First release.
Explanations
- Line-by-line explanations of shell scripts in English and Japanese: commands and options (about 120 commands and builtins, with combined flags, option values and subcommands), operators, redirections and their order,
${…}expansions part by part, control flow, and well-known idioms. - The languages inside arguments: regular expressions (BRE, ERE, PCRE), sed scripts, awk programs, jq and yq filters, perl one-liners,
printfanddateformats,findexpressions,chmodmodes,sort -k/cut -f/trsets,getopts/getoptoption strings, andgit log --pretty/--dateformats. - Worked examples with sample input and output, for regexes, sed, awk, jq, yq, perl, text filters, whole pipelines, expansions,
printf, globs, tests,xargs,read,getoptandgit log. They are computed by built-in code that the tests check against the real tools. - Your own commands, described in
bashExplainer.customCommands.
Ways to show them
- A hover for the line under the mouse, or for the cursor line with
Ctrl+K Ctrl+E(Cmd+K Cmd+Eon macOS). - A panel that follows the cursor, inspired by regex101: edit an example's input and watch the output change, with matches and capture groups highlighted; fold the tree, copy it as Markdown, switch the shell or yq for the file, and search a quick reference.
- An inline one-line summary at the end of the cursor line.
Other shells and other files
- Constructs that POSIX sh, dash, ash, yash, ksh, mksh or zsh do not support are pointed out, and behaviour that differs from bash is noted. The shell is detected from the shebang, a
# shellcheck shell=line or the file name. - Shell code inside Dockerfiles, Makefiles, GitHub Actions and GitLab CI YAML,
package.jsonscripts, Markdown code blocks and Pythonsubprocess/os.systemcalls is explained too, with warnings about script injection, DockerRUNpipes withoutpipefailand Python commands passed withoutshell=True. - yq commands are recognised as mikefarah/yq v4, kislyuk/yq or mikefarah/yq v3, with the first v4 release that has what the command uses.
Problems panel
- Warnings in the Problems panel, with quick fixes where the rewrite keeps the meaning (
[[ ]]→[ ],==→=,source→.,&>→> … 2>&1, and more). # bash-explainer disable=<code>anddisable-file=<code>comments, andbashExplainer.diagnostics.ignore, to silence them.
Privacy
- Works offline by design: no network access, no AI, no telemetry, no external processes. A CI check keeps it that way.