Skip to content

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 02:56
· 1058 commits to main since this release
  • 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 rounded up to the unit, so -size -1M is only empty files and a bare -size 10 counts 512-byte blocks; -perm 644, -644 and /222 are told apart; -newermt, -newerXY, -anewer, -cnewer, -samefile and -links are known (before, "Option (not in the dictionary)" and their values read as options); -maxdepth after a test gets a note that it is global. Before, these read "modified more than 7 days ago", "smaller than 1M" and "permissions 644".
  • date +%3N is the first 3 digits of the fractional second (%s%3N is milliseconds since the epoch; before, "nanoseconds width 3" and a wrong example), and %:::z is shown as +00 instead of being left as text.
  • Smaller fixes in examples: stacked quantifiers in grep and sed patterns mean "repeat the repeated" (grep -E 'b+?' matches every line, as (b+)?; before, the JavaScript lazy b+?), xargs -d , keeps the newline that ends the input in the last item (c.txt\n; before, dropped), and git log --pretty=format:%B no longer ends with an extra blank line. English ordinals are written right (2nd, 3rd, the last field) in sed, jq and perl.
  • A construct is labelled with its skeleton instead of its text cut short: the keywords and operators, the short parts as they are written, and an italic placeholder for what is left out. if [ -z "${1:-}" ]; then … is now if condition then commands elif condition then commands else commands fi; a loop is for f in words do commands done (while and until keep a redirection: done < input.txt); case "$1" in patterns esac; a function is backup() { commands }; groups and subshells are { commands } and ( commands ); a pipeline or list names its commands (grep | sort && echo || exit); a command with redirections is tr < windows.txt > unix.txt, with file for a target longer than 30 characters. !, time and coproc come with their command. A [ ], [[ ]], (( )) or assignment longer than 30 characters is [[ expression ]] or x= value; shorter ones keep their text. The placeholders are English in every language; clicking or pointing at the label still highlights the whole construct. The hover, "copy as Markdown" and the Language Server show the placeholders in italics, the panel and the web page in a muted italic, the command-line tool in italic and dim on a terminal and as ‹name› without colour, and --format json has the parts as labelParts next to the plain label. Before, the label was the text of the construct cut at 60 characters (if [ -z "${1:-}" ]; then …), which hid what the construct did.
  • A subcommand is a token of its own kind, subcommand (git log, docker run, kubectl get, npm run…). It is drawn in the colour of a command everywhere, and its explanation keeps the usual description and adds a note on the syntax: "Subcommand git log: shows the commit history (to the shell it is an argument to git, which interprets it itself)". For a deeper level the note names the whole chain before it (git remote). Before, a subcommand was a plain command and nothing said that the shell sees an ordinary argument.
  • [ … ] shows that it is the command [ (an alias of test), not shell syntax. Its explanation starts from that: spaces are needed around [ and ], and the arguments undergo word splitting and globbing ([[ ]] does neither). The parts are now the [ itself (a builtin), the condition as before, and the closing ] (an argument: "the last argument passed to ["). New pitfall notes cover [-f x] (the name is [-f), [ -f x] (the ] is part of the word), [ $a < $b ] (a redirection) and [ a && b ] (&& ends the command); an unquoted variable in [ ] was already pointed out. The explanation of [[ ]] adds that it is shell syntax, so it has no word splitting or globbing and < and && work. Before, [ ] was "test [ ]" with only the condition under it, and none of these four mistakes was pointed out. test -f x is unchanged.
  • A glob pattern is explained the same way wherever it appears: the right side of =, == and != in [[ ]], case patterns, arguments (ls *.txt, rm -f log-202[0-4]-??.txt), the patterns of ${x#pat}, ${x##pat}, ${x%pat}, ${x%%pat}, ${x/pat/rep} (and //, /#, /%), extglobs, and the quoted pattern of find -name. The pattern says it is a glob (shell wildcards, matched against a string or against file names), with a child for each part: * (any string), ? (any one character), [abc] / [!a-c] / [[:alpha:]] (one character of a set), an extglob @(a|b) with its alternatives, a literal run, an escaped character and a quoted part. [[ $x = *.txt || $x = *.sh ]] also shows, for each pattern, values that match and values that do not, as case does (these replace the single true/false pair [[ $x == *.txt ]] had). Before, the [[ ]] pattern was only "Pattern", case described just * and ?, and ls *.txt explained no part of the glob.
  • The example of a jq or yq filter with top-level pipes shows the data after each stage, as the example of a shell pipeline does: for jq '.items[] | select(.price > 10) | .name' it shows the sample input, the values after .items[] (one per line), after select(.price > 10), and the output after .name. Pipes inside select(…), map(…) or […] do not split. The panel's editable input updates every stage. If a stage cannot be run on its own (. as $x | …) or by the example runner, the example is the same as before: the input and the output only.
  • The web page can compare two versions side by side. compare.html (linked from the banner of a pull request's preview as "Compare with main", and from the banner of the development version as "Compare with release") has the editor on the left, the explanation of this version in the middle and that of the other version on the right (main for a preview, the release for the development version; ?with=release or ?with=next chooses), each under its name ("PR #123 (abc1234)", "main (def5678)", "Release v0.5.0"). What you type or select goes to both panels, and a click in either panel selects in the editor. It opens with your script and language. A version built before this one has no panel-only view (?view=panel, which every page now has) and says it cannot be compared. The comment on a pull request also links to the comparison. Before, comparing a pull request with main meant opening two pages and typing the script in both.
  • Each open pull request of the repository now has a preview of the web page at https://sf624.github.io/bash-explainer/pr/N/ (N is the pull request number), built from its head, with a banner that links to the pull request and the commit, and a comment on the pull request links to it. The preview is updated when the pull request is pushed to and removed when it is closed. Before, a change to the page could only be tried by checking out the branch. BASH_EXPLAINER_CHANNEL=pr BASH_EXPLAINER_PR=N npm run build:web (or --channel=pr --pr=N) builds one. The commit link in the banner of the development version now points to the commit; before, it had no commit in its address.
  • On a phone, the web page is easier to use. Tapping a row of the explanation selects its token in the editor and scrolls to it, but no longer focuses the editor, so the soft keyboard stays closed (a click on a problem in the list does the same; with a mouse or a pen the editor is still focused). The toolbar is three rows of two (File | Language, Version | Theme, Load | Copy), each control as wide as its longest option so that no text is cut and 44px high (the labels are kept for screen readers but not drawn), and the links are larger to tap; the token's start, not its end, is scrolled into view, so a long line is not pushed left out of the editor. In a narrow panel (520px or less) a long label is no longer cut with "…": it stays on the line of its fold mark (cut with "…" if too long), and the explanation goes under it when less than 60% of the row is left beside it (and breaks inside a long word); the panel's buttons are larger. Before, a tap opened the keyboard every time, the toolbar took five rows of 24px controls, a reveal scrolled the editor to the end of the token and hid the start of every line, and a label such as config.yml{,.bak} was cut to config.yml{,.ba… with the explanation squeezed beside it.
  • The web page has a theme switch next to the language switch: Auto (the system's colour scheme, as before), Light and Dark. The choice is remembered in the browser (localStorage; without it the page stays on Auto), is applied before the page is drawn, and covers the editor and the explanation panel. Before, the page could only follow the system.
  • The web page now comes in two versions. https://sf624.github.io/bash-explainer/ shows the latest release, as before; https://sf624.github.io/bash-explainer/next/ shows the development version built from main, with a banner that says so and links to the commit. A "Version" switch in the toolbar moves between them, keeping your script and the language. The development page is redeployed whenever main changes the page, and the root only when a version is released; before, the page was rebuilt only at a release, and main could not be tried in the browser. npm run build:web builds the release page; --channel=next (or BASH_EXPLAINER_CHANNEL=next) builds the development one.
  • In the panel (and on the web page), a faint vertical line joins an item to its next sibling, as the indent guides of VS Code's trees do, so that you can see which items are on the same level. It runs in the column of the rows' coloured left border, beside the item's parts and examples, and, like the rule beside an example, has the same faint grey and leaves the same gap to the rows above and below it; the last sibling has none. The left rule of an example under a token now lines up with the border of the token's parts (it was 1.6px to the right), and it starts as far below the row above it (0.5rem) as it ends above the next one (it was 0.3rem). Before, nothing joined the items of a level, and it was hard to tell where a long group of parts ended.
  • Escapes that stand for a control character name it, and printf colour codes are explained as ANSI sequences. \033, \e, \x1b, \u001b and \0033 read "the character with octal code 033 (ESC, escape)" (Japanese: 8進数 033 の文字(ESC、エスケープ文字)), and the same for NUL, BEL, BS, HT, LF, VT, FF, CR and DEL, in printf formats, tr sets and git %xNN. In a printf format, \033[1;32m is one token, "ANSI escape sequence: bold, green foreground", with the escape and the literal text under it: it knows reset, bold, dim, italic, underline, blink, reverse and their resets, the 8 and 16 colours for text and background, the defaults, and 256-colour and RGB; other sequences (\033[2K) read "CSI …". An example that shows a control character as a picture (␛, ⇥, ␡) now says once what it stands for ("␛ is ESC (escape)"), in the hover, the panel, the command-line tool and the web page. The same holds for the arguments of echo -e and for $'…' strings, which now have a child for each escape (\e, \033, \0033, \x1b, \u001b, \n, \t, \c, \cA, each read as bash reads it) and the ANSI parent. GNU sed escapes in s///, addresses and replacements (\x1b, \o033, \d27, \cA, \t, \n, \a…) are explained as the character they stand for, and the worked example of sed 's/\x1b\[[0-9;]*m//g' strips the colour codes. Before, printf '\033[1;32m%s\033[0m\n' OK gave ␛[1;32mOK␛[0m with no hint where ␛ came from, and \033 was only "the character with octal code 033". echo -e and $'…' had no breakdown, and sed 's/\x1b/…/' read \x1b as the letters "x1b" (its example showed the input x1b[0m and an empty output).
  • On the web page, a selection on the line of the cursor (made with Shift and the arrow keys, or by dragging within one line) is now highlighted. Before, the highlight of the current line covered it, so only selections over several lines showed.
  • An option that takes a value no longer calls it "this directory" or "this file". tar -C says "指定したディレクトリに移動してから処理" / "change to the given directory first" (also make -C, git -C, grep --exclude-dir, -o FILE, -u USER, -n COUNT and about 190 other options in all); before, "このディレクトリ" / "this directory" could be read as the current directory. A test keeps the wording out of the dictionaries.
  • In the panel (and on the web page), a click anywhere on a row that has parts folds or unfolds it, as in VS Code's trees, and still selects the token in the editor; a row without parts only selects. A drag to select text, and the ▾ itself, do not change. The fold also stays when the panel is drawn again for the same line (another shell chosen, for example). Before, only a click on the ▾ folded a row, although the whole row showed a hand cursor, and a redraw unfolded everything.
  • The example of a whole pipeline picks sample lines so that every stage changes the data it gets. For grep -i error app.log | sort | uniq -c | sort -rn | head -n 3, the sample has an ERROR line (matched only because of -i) and four different lines with different counts, so sort -rn reorders them and head -n 3 cuts one; grep -v, -w, -x, -m, tail, and sort/cut followed by head get lines that show their effect in the same way. The sample is grown only while every section fits in 12 lines, and the example now shows up to 12 lines of a section (it cut after 7 lines before). If more lines cannot make a stage change anything (a default head after uniq -c would need 11 distinct values), the earlier sample is kept. Before, the sample had two distinct lines, so head -n 3 passed everything and -i made no visible difference.
  • Positional arguments are described by how many there are. seq 5 says the 5 is the last value, seq 2 5 a first and a last, seq 1 3 10 a first, an increment and a last; before, they read "first (or last, if only one argument)" and "increment or last". The same for the destination of cp, mv, install, scp and rsync (cp a b copies to b, cp a b c d/ into the directory d/; with -t DIR all are sources; before, "source or destination"), ln (ln t makes the link here, ln t l names it, ln a b dir puts the links in dir), basename (-a makes every argument a path), tr -d (one set), mount /mnt (a device or a mount point) and rsync s (lists). cmp describes the two skip arguments, and join its two files; before, cmp a b 10 20 called them all "target file".
  • ${{ github.event.… }} in a GitHub Actions workflow now says what the field is and for which event: about 50 fields of the pull_request, issues/issue_comment, push, release, workflow_run and schedule payloads, inputs.<name> of workflow_dispatch, and repository/sender. Any other github.event.x.y is described as a field of the webhook payload of the event (named when x tells it, e.g. pull_request). More contexts are explained too: runner.name, job.container.id, job.services, strategy.fail-fast, github.workflow, github.workflow_ref, github.triggering_actor, github.ref_type, github.ref_protected, github.event_path and github.action_path. The script injection warning now also covers label names, release names and bodies, committer names and e-mail addresses, and commits[0].…. Before, every github.event.… path other than a few was only "replaced with the value of …".
  • A simple command is labelled with its name, not its whole text: grep -E 're' ids.txt is shown as grep with -E, 're' and ids.txt under it (the same for builtins, functions, declare/export, sudo's and find -exec's command, and a name such as /usr/bin/env, "$cmd" or $(CC)). Clicking it or pointing at it still highlights the whole command. This shows in the hover, the panel, "copy as Markdown", the command-line tool, the Language Server and the web page. Before, the label repeated the whole command, which the description (of the program) did not describe. For tr -d '\r' < a > b, the command under the redirect token is now tr.
  • The explanation of a token no longer repeats its label. In printf '\033[1;32m%s\033[0m\n' the parts of the format read \033 — the character with octal code 033, %s — value 1 as a string; the same in regexes, awk, sed, jq, chmod modes, getopts, ${x@Q} and the like, in English and Japanese, in the hover, the panel, the command-line tool, the Language Server and the web page. Before, each began with its own text again (\033: \033: the character with octal code 033). An explanation that quotes something else or only mentions the label in a sentence is unchanged, and notes, summaries and examples keep their text.