Skip to content

Releases: sf624/bash-explainer

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 02:02

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 with bashExplainer.diagnostics.pitfalls.
  • New warnings and caveats, among them: curl … | bash, a variable read while it is unset under set -u, a shebang whose options bash refuses (#!/bin/bash -e -u), find . -exec rm {} ;, a trap on INT that does not exit, an assignment to a read-only variable, and cmd || 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 with bashExplainer.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 follows exit or return. In POSIX mode, a line where bash stops the script is a warning. Turn the fading off with bashExplainer.hints.deadCode.
  • The new setting bashExplainer.bashVersion (--bash-version, the Action's bash-version) explains scripts for bash 5.2 (the default) or 5.3: ${ cmd; }, ${| cmd; }, and what bash 5.3 changes in builtins, variables, printf and [ ]. Compatibility: with the default 5.2, a bash 5.3 construct (${ …; }, ${| …; }, shopt -s array_expand_once) is now a warning, and --check exits 1; set bashExplainer.bashVersion to 5.3 to 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 $1 in 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_SUBSHELL does not count the subshell of a pipeline stage.
  • The new setting bashExplainer.panel.colors colours 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, after setup), 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 -h and 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 rm is 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+E or 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+E and the name hints instead.
  • Compatibility: bashExplainer.highlight.enabled is now true by 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: --check prints the new pitfalls but exits as before: their warnings fail it only with the new --strict (the Action's strict: 'true'), the Action's warnings output still counts only what fails the step, and its new pitfalls output counts the pitfalls. In the JSON output, an alias's token kind is now alias (it was function).
  • 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 (time and ! 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 IFS in effect (** without globstar), in case patterns, 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 <<B reads only B, exec >log 2>&1 applies to the shell, exit 1 2 goes on, and builtin cd is the builtin itself. Every option of bash's builtins, and every shopt option, 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 for x = 1, rm -rf $DIR/* is no warning when the script set DIR, and an alias in 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 under sudo, xargs and a quoted command name too.
  • Options and operands are read as each tool reads them: docker run -it is -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 -n with q or n, and a quoted command name (\sort).
  • Shell code in other files: a YAML block (run: |) is read without its indentation, each workflow step, Dockerfile RUN and Makefile recipe line is a shell of its own, and notes name the right line. Before, an indented EOF did not end a here-document, and set -u in 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 F6 goes 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 as toString no longer make the explanation fail.

All changes

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 24 Oct 00:56

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 '…' and watch '…', also under sudo, xargs, find -exec, docker exec and kubectl exec. Its commands and expansions appear at their own characters, its notes are those of the shell that runs it (an sh -c string gets the POSIX checks), and the Problems list shows its dialect issues. What a static eval string 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: "$mode is fast here, so this condition is always false: the commands after its then never run", or "so only fast) of this case runs". 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 a bats shebang) are explained: @test "…" {, run, load, skip and the other bats commands, with pitfalls for a ! cmd that cannot fail a test and for run in a pipeline. VS Code, the Language Server, the command-line tool (--kind bats) and the web page (with a bats sample) recognise them. Before, @test was a syntax error and run an unknown command.
  • env -S is read as GNU env reads it. A shebang #!/usr/bin/env -S bash -e runs bash -e with the script. #!/usr/bin/env bash -e warns that Linux passes bash -e to env as one word, which fails. An env -S string 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, yash and ash (also by path) take the options of sh: 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.json reads only a.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.maxLength under 20, bashExplainer.hover.maxDepth or hover.followDelay under 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 after break, continue, return or exit no 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 +%F says "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 n and -S are followed), and reads -R, input and inputs. mikefarah/yq prints JSON as yq does, reads a .json file as JSON, prints strings bare with -r, and shows with -i what the first file becomes. awk gives FILENAME the name of each file. sort -o and uniq in out show what is written to the file. Before, jq and yq printed every result on one line, and FILENAME was 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 -n and an awk program of BEGIN rules only show no input. Several files (sort a b, head -n 1 a b) show a section each. cat and nawk get real examples. Before, the example made up one input for all.
  • Worked examples follow the command's options. grep -e -v foo searches for -v, sed -ie 's/a/b/' -e 's/c/d/' f runs only s/c/d/, and find … -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 --almost is --almost-all) and know every long option of their --help. test reads no options, xargs --max-lines echo runs echo, perl -0ne is -0 -n -e, and bash -O extglob -c '…' and bash --posix are 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 set x, exec {fd}>file, wait -p var and coproc name set their variables, and a division by 0 in $(( )) discards the rest of the line. unset f removes the function when there is no variable f. exec cmd in ( ) 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 of for (( i = 0; … )), $_ is no longer said to be empty.
  • [, unset and declarations are explained as what bash runs, however they are written: \[ -f x ], command [ -f x ], x=1 unset x, \declare a=$v. [ -f x with no ], and words test cannot read ([ a b c d ]), say that the command fails with status 2. unset 'a[1]' removes an element, and an unquoted a[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, and unset of 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, so x=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 as a=1 >f b=2 left the whole script unexplained, and such commands had the status 0.
  • The ⚠ notes follow what bash does here. After IFS=; set -f, cp $a b no longer says $a is split. A variable set by printf -v or mapfile at the end of a pipeline is reported as lost. The pitfall for a variable another shell does not have sees \$n in bash -c "echo \$n", and knows that env -u x removes x. egrep -q counts as grep -q, and an alias of rm as rm.
  • 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 - EXIT sets no clean-up, while [ $# > 0 ] writes to a file named 0, 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 above run:, and defaults: or runs-on: count after the steps. A Dockerfile or Makefile SHELL and a Python executable= 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, or env -S with bash --posix). Before, --posix anywhere on the shebang line counted, also in #!/usr/bin/env bash --posix, where bash does not start.
  • With notes.pitfalls off, more ⚠ notes are hidden: those for distribution aliases, \b in [[ =~ ]], a Dockerfile RUN without pipefail, Python subprocess, GitHub Actions ${{ … }} injection and bats files. Before, these ignored the setting.
  • Quick fixes are offered in every language of the bashExplainer.languages setting, as the hover is. Before, they were offered in shellscript documents 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...
Read more

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 18:23

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: set and shopt options, variables (also those set by arithmetic, such as (( i++ )) and let, 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 builtin or command), enable -n, hash -p, special builtins in POSIX mode, names that may come from a sourced file, and functions defined in a subshell or removed with unset -f.
  • New notes on subshells and other shells: cd, set -e or exit in a subshell only change the subshell, and $$ there is still the script's PID. Code run by bash -c, sudo, su -c, ssh or docker exec gets 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 passing declare -f).
  • New set -e notes: x=$(cmd) stops the script on failure but local x=$(cmd) does not; a pipeline without pipefail hides a failure; a function ending in test && cmd returns the test's failure; and the places where bash ignores set -e.
  • Idiom notes for getopts, getopt with eval set --, while [ $# -gt 0 ], read -d '', find -print0 / xargs -0, mapfile, read -p, and while 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_CONFIG or ~/.config/bash-explainer/settings.json), with --set key=value, --no-config and --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.json file 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.customCommands now takes options as a list (each with its names) and operands as a list (with "repeat"), plus an optional style and subcommands. 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 named head no longer reads -5 as -n 5.
  • Markdown code blocks tagged sh or shell are 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 if or 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 -f on some paths, ambiguous redirection targets (> *.log), no globbing in x=*.txt, no word splitting in <<< $x or ${s:$i}, unquoted patterns in case and [[ ]], 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 cmd and watch 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, ar and /bin/echo or 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"). The xargs -0 -I{} example runs once per name, and jq 'def g(x' no longer stops the explanation.
  • A Dockerfile RUN warns about a pipe without pipefail unless set -o pipefail runs before the pipe; before, the word pipefail anywhere in the RUN, even in a comment, silenced it.
  • On the web page, the Problems list shows code as code instead of raw backticks.

All changes

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 02:56
  • The command a wrapper runs is found correctly, and the pitfalls of wrappers are pointed out. bash -c 'echo $1' a b (also sh, dash, zsh, ksh, and -xc, -o pipefail -c) says a becomes $0 and b is $1; bash script.sh a b calls a and b arguments $1 and $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 run and doas explain ls / cmd as the command that runs, with its own arguments. timeout 5 sleep 10 takes 5 as the time limit and sleep 10 as the command; env A=1 cmd, sudo A=1 cmd and doas A=1 cmd take A=1 as an assignment; nice -10 make reads -10 as the adjustment -n 10; su - user reads - as the login option. sudo cd /root and timeout 5 cd /tmp are both explained as cd. 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 as cd, export, source or read cannot run (with the fix: export -f and bash -c, or sudo sh -c '…'; also for a function named after ssh host or su -c); env FOO=1 echo $FOO and FOO=1 echo $FOO expand $FOO before the variable is set; ssh host ls $HOME/*.log is expanded by the local shell; { echo a; echo b } has no ; or newline before }, and {echo a;} has no space after {. Before, foo of bash -c '…' foo was "the script to run", ls of kubectl exec pod -- ls was "Pod", the 5 of timeout 5 cmd and the A=1 of env A=1 cmd were taken as the command (and cmd as its argument), nice -10 as the options -1 -0, sudo cd and timeout 5 cd were 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 15 and trap -p INT take signals, kill -n 9, wait -p v, read -N 3 / -i text, exec -a name cmd, and all value options of mapfile / readarray take their values, getopts names the words to parse, . file args the arguments, and trap INT resets the signal. %1, %% and %name are job specs. set -o / set +o alone list the settings. export -n removes the export property (it said "name reference"), export and readonly have their own options, declare +x turns an attribute off, unset -n is an option, fc, bind and suspend are builtins, jobs -l, disown -h, type -p, hash -r, declare -t and 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, >& and test are explained correctly. LC_ALL=C export X and V=1 declare -x Y stay builtins (they were "external command export"); with -f or -F the names are functions (export -f fn); every declaration builtin and unset carries "(shell builtin)"; cmd >& file says stdout and stderr; the example of [ $# -eq 1 ] says "1 argument" (it was #="1"); an alias in a bash script without shopt -s expand_aliases is "command not found" (it said "replaced by …"); under #!/bin/sh or set -o posix, an assignment before a special builtin (V=1 :) stays after it (it said "only for this command"); the words of test are 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]} with a[1]=hello is 5; before, the scalar a=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 **/*.txt on one line has globstar on (before: a pitfall note), and so on for dotglob and nocaseglob. unset IFS and a value that is not known (IFS=$old) end the earlier IFS note; set -f ends "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 a coproc is said to run in a subshell. []a] is a bracket expression, not "no wildcards". : ${V:=x} assigns also when V is empty.
  • New notes: ((i++)) with i at 0 has status 1 (set -e stops); exit 256 / return 300 are taken modulo 256; local outside a function fails. A missing ; or newline before then or do (if [ -f a ] then …, for x in a b do …) is reported as a syntax error; before, then/do looked like an argument and nothing was said.
  • Options and operands of external commands are explained correctly in more places: kubectl logs -f POD (-f is --follow, not --filename), git tag -a -m MSG TAG (-a takes no value), rpm -ivh (-i is install; -qi is info), chmod/chown/chgrp --reference=RFILE FILE (every operand is a file), fdisk -o (output columns; -w|--wipe is the wipe option) and fdisk -u -l (-u takes 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, and pip uninstall/show (a package name, not a pin). New: roles of the operands of uniq, zip, unzip, mktemp and pgrep/pkill, and unzip -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' ' and awk -F"\t" (the value reads ", after quote removal", and a -e/-F value 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, --slurpfile and --rawfile take a name and a value (also in kislyuk/yq), --indent N, -f FILE (then the first operand is an input file), --args and --jsonargs (the operands are strings or JSON values, not files), --tab, -S, -a, -j, --raw-output0, --seq, --stream, -C, -M and -L. The example follows -s (the file holds the values, one per line, and the filter reads them as one array: jq -s length gives their number) and -j (no newlines). Before, jq --arg n foo '.name = $n' in.json took foo as the filter, and -s and -j were ignored.
  • jq: destructuring (. as [$a, $b], {k: $v}, {$a}, {(expr): $v}, ?//, in as, reduce and foreach) and label $x | … break $x are 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/indices on an object give null (and fail on a number or a boolean); fromjson gives jq's message for a non-string. Before, these filters were "Could not parse", the sample input made .[] | @csv and contains("a") fail, and test("v a"; "x") was wrong.
  • In [[ $x =~ \bfoo ]] the unquoted backslash before an ordinary character is removed by bash, so \b, \w, \s, \d and \< are the plain characters b, w, s, d and <; 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*/-/g on x xx gives - - (before, -- --), s/b*/X/2 on b bb gives b X; 0,/re/ can end on line 1 (before, 0 read as "line 0" and the example ignored it); sed -z shows no example (before, it was run on lines); s/o/0/2g says "every match from the 2nd on" (before, "every match"); an empty pattern (s//x/) says "the last regex"; an incomplete \{ in grep '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=value operands (awk '{print $1,$2}' OFS=, f) are described as assignments and used by the example (before, "target file"), and escapes in -v and -F values are processed (-v OFS='\t' is a tab, -F '\\|' splits at a bar); $0 = … says that the fields are split again, delete a that the whole array is emptied, and RS = "" that it is paragraph mode. An example that needs another RS, a CONVFMT, the 4th argument of split or more printf formats than values is not shown, as it could not match awk.
  • printf in bash and awk examples matches C: %#x and %#o (0xff, 010), a negative number in %x, %o and %u (two's complement, ffffffffffffffff), %.0d of 0, and %.3e of 12345 (1.234e+04, ties go to the even digit as in glibc; before, 1.235e+4). bash's printf %d 1.5 and %d 1e3 ("invalid number") no longer get an example.
  • find is explained as it behaves. -mtime +7 is "at least 8 days ago" (days are whole, the fraction is dropped; -mtime 7 is from 7 to 8 days, 0 the last 24 hours); -size is ...
Read more

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 15:21
  • 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 .vsix is a quarter smaller (703 KB, was 947 KB) and the web page downloads 1.5 MB less (no .wasm files); 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 is EOF, not EOF;), 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, iptables options, 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 -e is not in OpenBSD netcat, shutdown -t and -F are ignored by systemd, mail -a attaches 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…). systemctl and ip explain their global options after the subcommand too (systemctl status -l x); before, only the options before it were known. /usr/bin/time ls is explained as the external time.
  • Options written without a dash in the first argument are explained letter by letter: tar cvzf a.tgz dir, ps aux and ps axjf (BSD options, whose letters mean something else than -a -x -u), ar rcs lib.a x.o, jar cvf, dump 0uf and restore 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 1234 and ps -ef are not); before, it was "Word cvzf". ar, jar, dump and restore are in the dictionary, and tar knows more options (-r, -u, -A, -d, -p, -k, -m, -O, -h, -P, -a, -b, -T, -X).
  • A command with redirections is now a redirect token that holds the command and its redirections side by side, as { …; } > f already was: for tr -d '\r' < windows.txt > unix.txt, the description and the worked example are on tr -d '\r', and < windows.txt and > unix.txt are 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 (fi becomes fi\r), so the script fails. The warning underlines the last word of the line, and # bash-explainer disable-file=crlf anywhere in the file turns it off. Before, the extension said nothing, and the unbash path read fi as a command.
  • An alias is used only on the lines read after the alias ran, as in bash: alias e='echo A'; e on one line, e before the alias, and a function or { } body read before it are explained as the command e (with a note), not as the alias; eval e reads the string again, so it can use it. A later alias of the same name replaces the earlier one. Before, an alias applied to the whole file, and the first definition always won.
  • set -eou pipefail and set -oeu pipefail are explained as bash reads them: an o anywhere in a bundle takes the next word as the option name (pipefail), and the other letters (u, e) still apply; the same for set +eo pipefail. Before, -eou lost the u and -oeu was explained as -o alone. The "strict mode" note is shown whenever one set command 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 pipefail was 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 bare echo ( is still accepted without an error, where bash reports a syntax error.
  • declare a[1] and declare 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." 2024 is 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 ' ' in tr -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 with Unsupported: raw byte. Other formats that the example cannot show (\x without digits, %q with control characters…) too.
  • A case pattern list that goes on after a backslash (a|b|\ then c)): the later line shows the pattern c. 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) or declare -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] = x and x[1 + 1]=x are 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, @a and @k is smoother (it had 「値を…を…する」).
  • [ 1+1 -eq 2 ], [ x -eq 2 ] and [ 1.5 -eq 1 ] say that [ ] reads only integers for -eq and 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, [']']=3 or ['q r']=z in h=( … ), 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']=c and [a\]]=c work 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 [ ] and test are 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 need shopt -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, so echo "`echo \"x\" y`" shows the string x (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"."c say that the quoted ...
Read more

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 09:47
  • 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.
  • -a and -o in [ ] 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 file was an "arithmetic unary operator". -g, -u, -k, -O, -G, -N and -R are 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/-p loop with last (perl -ne 'last if /^END/; print') get worked examples, as perl runs them: the loop stops, END still runs.
  • jq filters with .a.[0] (jq 1.7) or a def after a comma (1, def f: 2; f) are explained; before, the whole filter was "could not parse".
  • git log --date=relative examples 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 { … } or END { … } 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>&- and exec 3<&- are explained again as closing the descriptor; since the parser update in 0.3.0 they were only "redirection >&-".
  • A Jenkinsfile sh step 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 as src/lib/error.ts. When no shopt -s globstar comes before it, a pitfall says that bash then treats ** as * (one level only), unless a file read with source turns it on; after shopt -u globstar, and in shells without globstar (sh, dash…), it is explained as *. zsh always has it. Examples no longer show names like appapp/app.ts. The options of shopt (globstar, nullglob, dotglob, extglob…) are explained.
  • Glob examples follow shopt -s dotglob (names starting with . match too), nocaseglob (file names ignore case) and nocasematch (case and [[ == ]] 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 *.txt as a command. They are explained part by part, with matching and non-matching names. A pitfall points out a missing shopt -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" ] || continue or shopt -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 suggests done < file, < <(command) or shopt -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 without shopt -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 $; unset explains its options and names; a Python shell=True script with \x41 or \u0041 escapes is decoded as Python does; perl one-liners with elsif get worked examples.
  • The pitfall for rm -rf "$DIR"/… no longer fires on special parameters such as "$$/x", where it suggested ${VAR:?}.
  • git log examples 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) and s{a} {b} are split into pattern and replacement as perl does, and s/a// reads "delete" instead of "replace with ``".
  • awk: the pattern 0 is explained as always false; it was "always true (non-zero)".
  • Regex examples: a{,2} in grep -E/sed -E (and a\{,2\} in basic regexes) is read as "at most 2", as GNU tools do, instead of as the text {,2}; in grep -P, []a] is a class of ] and a instead 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 like appapp match, which the examples said did not.
  • mikefarah/yq v3 paths: (.==x) is explained as the elements equal to x, instead of the elements whose . matches x.
  • sed examples follow GNU sed in more cases: \L/\U cancel 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 (\9 for a group the pattern lacks, // with no regex used yet) get no example instead of a made-up output.
  • Shell dialects: select loops and read -n1 (a value attached to -n, -t…) are noted in scripts for shells that lack them; the parser reads select as a for loop, 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 -h puts 0K with the zeros, as GNU sort does, and tr a '' gets no example (GNU tr rejects an empty second set).
  • Pipeline and awk examples: grep -o -v prints nothing, as GNU grep does, instead of the non-matching lines; awk -Ft splits on the letter t, as gawk and mawk do, not on tabs (-F'\t' is a tab).
  • Host files: a GitHub Actions step with shell: pwsh (or any shell: of its own) is no longer read with the shell: of the step before it; a Dockerfile exec-form string with \n no 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) is he); \\ in a sub()/gsub() replacement stays two backslashes unless & follows; programs gawk rejects (an array used as a value, (a, b) outside in, a newline right after (, a built-in without its argument) get no example.
  • perl examples follow perl in more cases: ! and not gave the opposite truth value; reverse 1..3 reverses the list; split " " and split alone split on blanks (the latter crashed); a match in list context gives its groups (my @m = /(\w+) (\d+)/); tr/a/b/s squeezes only translated characters; next inside a for loop goes on with the next item; with -F, fields are split with the newline, as perl does; "\Lab\uc" is abc. 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: round takes halves away from zero (-2.5 → -3); try keeps the values output before an error; an error on the left of // is reported, not replaced by the default; @base64d fails 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.
  • time is explained as bash's keyword that times the command after it (-p for 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...
Read more

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 12:50
  • 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, shellcheck and 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's defaults and hdiutil, and more; docker buildx/system/inspect, gh pr options, 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=x and go test -count=1: single-dash long options with =value are read.
  • head -1, tail -5 and git log -3 are read as -n N.
  • sed -i.bak takes .bak as the backup suffix instead of more options.
  • gawk, mawk and nawk are explained as awk.
  • date +FORMAT gets 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 GNU date. Turn it off with the date kind in bashExplainer.examples.disabled.
  • Pitfalls are pointed out where they appear, with how to avoid them: x = 1 (a command, not an assignment), rm -rf $DIR/… when DIR may be empty, unquoted variables as paths and inside [ ], for f in $(ls) / $(find), for a in $*, read without -r, local x=$(cmd) hiding a failure, and sudo cmd > /etc/…. In about 165,000 lines of the surveyed scripts they were found about 400 times. Turn them off with bashExplainer.notes.pitfalls.
  • More CI systems: Jenkinsfile sh steps (in every quoting, with the $name and ${…} that Groovy fills in pointed out), CircleCI run: steps written with name: and command:, and Azure Pipelines script: and bash: 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 says shell: bash), and Dockerfile stages that use PowerShell or cmd (SHELL ["powershell", …], Windows base images). SHELL now applies until the next FROM, 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

Choose a tag to compare

@github-actions github-actions released this 17 Oct 07:33
  • A command-line tool, the npm package bash-explainer: npx bash-explainer deploy.sh:12 explains lines in the terminal (text, Markdown or JSON), and --check reports what the script's shell lacks, as compilers print warnings.
  • A Language Server for other editors: bash-explainer --lsp gives 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

Choose a tag to compare

@github-actions github-actions released this 16 Oct 18:43
  • 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

Choose a tag to compare

@github-actions github-actions released this 16 Oct 17:07

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, printf and date formats, find expressions, chmod modes, sort -k / cut -f / tr sets, getopts / getopt option strings, and git log --pretty / --date formats.
  • Worked examples with sample input and output, for regexes, sed, awk, jq, yq, perl, text filters, whole pipelines, expansions, printf, globs, tests, xargs, read, getopt and git 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+E on 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.json scripts, Markdown code blocks and Python subprocess / os.system calls is explained too, with warnings about script injection, Docker RUN pipes without pipefail and Python commands passed without shell=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> and disable-file=<code> comments, and bashExplainer.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.