Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

- A VS Code 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.
- On the web page, changing the file kind changes only how the script is explained: the text stays, also an untouched sample (an empty editor still gets the new kind's sample). "Load the sample" loads the current kind's sample, and when it replaces your own text, a notice with an "Undo" button appears for a few seconds. Before, choosing a kind replaced an untouched sample with the new kind's sample, and "Load the sample" could be undone only with Ctrl+Z.
- A branch that cannot run, because its condition is decided by values the script itself set, no longer changes how the lines after it are explained: after `x=1; if [ "$x" = 2 ]; then set -f; fi`, a glob is a glob again, and after `if false; then f() { …; }; fi`, `f` is not the function. 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". A condition is decided only from variables that nothing but plain assignments (`x=1`, `export x=1`) writes anywhere in the script, and not at all in a script that runs what cannot be read (`source`, `eval "$s"`, `$cmd`, an indirection such as `${!v}`, `${!1}` or `${!v@Q}`, whose subscript may assign any variable, a trap whose action may `return` from where a signal comes): there, both sides still count. Arithmetic that reads one of bash's own numbers (`shift $((OPTIND-1))`, `(( EUID == 0 ))`) keeps the decisions (before, it turned off every decision in the script), and text assigned to `OPTIND`, `RANDOM`, `SRANDOM` or `HISTCMD`, which bash runs as arithmetic (`OPTIND='x=5'` sets `x`), counts as setting what it sets. Conditions in a trap's action are never decided.
- A branch that cannot run, because its condition is decided by values the script itself set, no longer changes how the lines after it are explained: after `x=1; if [ "$x" = 2 ]; then set -f; fi`, a glob is a glob again, and after `if false; then f() { …; }; fi`, `f` is not the function. 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". A condition is decided only from variables that nothing but plain assignments (`x=1`, `export x=1`) writes anywhere in the script, and not at all in a script that runs what cannot be read (`source`, `eval "$s"`, `$cmd`, an indirection such as `${!v}`, `${!1}` or `${!v@Q}`, whose subscript may assign any variable, a trap whose action may `return` from where a signal comes): there, both sides still count. Arithmetic that reads one of bash's own numbers (`shift $((OPTIND-1))`, `(( EUID == 0 ))`) keeps the decisions (before, it turned off every decision in the script), and text assigned to `OPTIND`, `RANDOM`, `SRANDOM` or `HISTCMD`, which bash runs as arithmetic (`OPTIND='x=5'` sets `x`), counts as setting what it sets. Conditions in a trap's action are never decided. The text of an alias is read with the command it is expanded at, and with what the script declares: `m[k]=1` in an alias after `declare -A m` keeps the decisions (before, it turned off every decision in the script).

### Fixed

Expand All @@ -37,7 +37,7 @@
- Quick fixes (the rewrites offered for dialect warnings) are offered in every language of the `bashExplainer.languages` setting, and follow a change of that setting, as the hover does. Before, they were offered in `shellscript` documents only.
- `"$@"` inside single quotes is text, so the words after `bash -c 'f "$@"'` are no longer said to depend on how many words it gives.
- Examples and dialect checks read a command's options as the command reads them. `grep -e -v foo` searches for `-v`; its example said `-v` inverts the match. `sed -ie 's/a/b/' -e 's/c/d/' f` runs only `s/c/d/`, since `e` is the backup suffix of `-i`; the example ran both scripts. `sed -i.bak` now shows the file rewritten. The input of `grep -efoo data.txt | sort` and `awk '{ print }' v=1 | sort` is labelled as it is read (a file, then stdin), and a pipeline whose later stage reads a file instead of the pipe (`printf … | grep -e -v foo`) gets no whole-pipeline example. `tr --delete` is `-d`, and `find … -exec cmd {} '+'` runs the command once, quoted or not. The dialect check no longer takes `read -p -a v` for `read -a` (`-a` is the prompt), and finds `trap ERR`. The examples of sort, uniq, cut, tr, head, tail, seq, basename, dirname, wc, xargs, sed, grep, awk, perl and `read` read the options as the explanation does, so a long prefix (`sort --rev`, `sed --expr`), `tail +2` and `tr -C` now get an example. `xargs --replace echo {}` and `xargs --max-lines echo` run `echo` (before, `echo` was taken for the value), and `-i`, `-l` are followed. `seq 1 -w 3`, which seq refuses, gets no example; before, it showed output. `cat -n`, `tac -s x` and `rev FILE` in a pipeline, and `cat -n f | sort`, get no example; before, the example passed the input on as if they had no option. The `git log` example shows as many commits as `--max-count 2` asks (before, 3).
- Explanations match bash in more places: `x=1 ''` (a command name that is not found, so `x` is not set), the variables that `exec {fd}>file`, `wait -p var` and `coproc name` set, and an error in `$(( ))` (a division by 0) or an assignment to a readonly variable, which discards the rest of the line, also when it is in a function the line calls (before, only the rest of the command, or nothing). A name written in the option's own word (`printf -vx 2`, `read -rax`, `wait -n -px`) is the variable set (before, `x` kept its old value, and a condition on it could be called decided), and `wait -p x` unsets `x` before it sets it (before, `x` kept its old value when no job ended). `exec cmd` in `( )` or `$( )` says that it ends only the subshell (before, it said nothing of the subshell). `'exit' 3` with quotes has its exit status known, 3, as `exit 3` has (before, it was the status of an unknown command). `$(true)`, `$(:)` and `$(false)` count as empty only where the name runs the builtin, not after `true() { echo a; }`, an alias of it or `enable -n true` (before, `x=$(true)` was taken to set `x` to nothing there). Functions defined after `set -a +a` or `set foo -a` (where `-a` is an argument) are not exported, so conditions in them are decided (before, none were). In a function whose every call is in a subshell (`m() { cd /tmp; }; echo | m`), `cd` says "It may run in a subshell (where its function is called; what the script does not show may call it too)" when a sourced file, a trap's action or an alias may also call the function (before, "It runs in a subshell", although `. ./lib.sh` may call it in the shell itself); a word that names the function without calling it (`command m`, `declare -f m`, `type m`) no longer turns this into "may" (before, it did). `cmd &> >(tee log)` and `&>> >(…)` send both outputs to the process substitution, also after a block or a loop (`{ …; } &> >(tee log)`, `done &> >(tee log)`) and before the command name (`&> >(tee log) cmd`) (before, it was read as an argument of the command, and the redirection as missing its target; after a block or a loop it was a syntax error, and before the name it was the command).
- Explanations match bash in more places: `x=1 ''` (a command name that is not found, so `x` is not set), the variables that `exec {fd}>file`, `wait -p var` and `coproc name` set, and an error in `$(( ))` (a division by 0) or an assignment to a readonly variable, which discards the rest of the line, also when it is in a function the line calls (before, only the rest of the command, or nothing). A name written in the option's own word (`printf -vx 2`, `read -rax`, `wait -n -px`) is the variable set (before, `x` kept its old value, and a condition on it could be called decided), and `wait -p x` unsets `x` before it sets it (before, `x` kept its old value when no job ended). `exec cmd` in `( )` or `$( )` says that it ends only the subshell (before, it said nothing of the subshell). After a `trap` whose action calls a function, what the function sets may have changed, also when the script called it before from the same state (`f() { x=2; }; x=1; f; x=1; trap f USR1`: `x` may be 2 after it, as bash has once the signal comes; before, it was 1). `'exit' 3` with quotes has its exit status known, 3, as `exit 3` has (before, it was the status of an unknown command). `$(true)`, `$(:)` and `$(false)` count as empty only where the name runs the builtin, not after `true() { echo a; }`, an alias of it or `enable -n true` (before, `x=$(true)` was taken to set `x` to nothing there). Functions defined after `set -a +a` or `set foo -a` (where `-a` is an argument) are not exported, so conditions in them are decided (before, none were). In a function whose every call is in a subshell (`m() { cd /tmp; }; echo | m`), `cd` says "It may run in a subshell (where its function is called; what the script does not show may call it too)" when a sourced file, a trap's action or an alias may also call the function (before, "It runs in a subshell", although `. ./lib.sh` may call it in the shell itself); a word that names the function without calling it (`command m`, `declare -f m`, `type m`) no longer turns this into "may" (before, it did). `cmd &> >(tee log)` and `&>> >(…)` send both outputs to the process substitution, also after a block or a loop (`{ …; } &> >(tee log)`, `done &> >(tee log)`) and before the command name (`&> >(tee log) cmd`) (before, it was read as an argument of the command, and the redirection as missing its target; after a block or a loop it was a syntax error, and before the name it was the command).
- `unset f` says it removes the function when there is no variable `f`, as bash does. A pipeline whose stage is a function or an alias of the script (`sort() { tac; }; … | sort`) no longer shows the output of the program of that name as an example. Before, the first said "Unset the variable" and the second showed the output of the program, which the function does not give.
- The ⚠ notes on words and pipelines follow what bash does here. After `IFS=; set -f`, `cp $a b` no longer says that `$a` is split (the token already said it is not); `X=1 echo "\$X"` no longer warns that `$X` is expanded first (`\$X` is text); a variable set by `((count++))`, `printf -v` or `mapfile` at the end of a pipeline, or by a function called there, is now reported as lost after it, and `echo '$n'` no longer counts as a later read. An alias of `rm` is taken for `rm`.
- The "Idiom" notes are read from the commands, not from the text. They are found with other spacing, order and quoting: `while true ; do` is an endless loop, `done <f` ends a `while read` loop over a file, and `IFS= read -d '' -r`, `$(cd "$(dirname "$0")"; pwd)`, `command -v jq 2>/dev/null >/dev/null`, `exec >> >(tee -a log)`, `$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )`, `while [ ${#} -gt 0 ]`, `while (( $# != 0 ))`, `trap cleanup 0` and `[[ $BASH_SOURCE != $0 ]] && return` are known. They are no longer given where bash does something else: `trap - EXIT`, `trap '' EXIT` and `trap -p EXIT` set no clean-up; `read -rd'' f` reads up to an `f`, not a NUL byte; `while [ $# > 0 ]` writes to a file named `0` and does not end; and text in quotes (`echo '$(mktemp)'`) is not run. Before, each of these got the idiom's note.
Expand Down
2 changes: 1 addition & 1 deletion docs/design/semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ An expansion error ends a path too. An error of arithmetic outside `(( ))`, `let

These replace the earlier deny-lists (`eval` or `source` before a condition, a builtin given a name not known, an exported function, a loop in which such a thing happens), which three reviews each found one more way around.

**Strings run as scripts (4.13).** The walk runs a static `eval` string where `eval` runs, so its conditions are decided and its dead code said as elsewhere. It runs a trap's action where `trap` sets it (only for what it changes) and after the top level, but decides nothing in it and says none of it is dead: a signal may run it at any point, and an EXIT trap runs not only at the end or at `exit`, but also at `${u:?}`, at a `set -u` error and at a signal, with values the walk does not see there. The trust scan reads the same trees as the walk (the model's `Scripts`, `StringTrees`: each string is parsed once), as foreign text (a trap's action may run at any time); so what a statement of a string has that the walk does not run (subscripts, offsets that may fail, names read as arithmetic) is found as for the script's own statements, with what the whole script declares (`Trust.reads`). Only the tree of an alias's text with the rest of its command, which the walk parses as it expands it, has a scan of its own (`treeFacts`). A trap's action that may leave what a signal interrupts, otherwise than by `exit`, makes the script trust nothing: an error that may end the line around it (bash: `declare -i q; trap "q='1+'" USR1` discards the rest of the line `kill -USR1 $$` is on), or a `return`, `break` or `continue` that its own functions and loops do not hold (`trap return USR1` returns from the function the signal interrupts). An EXIT trap's action ends with the shell, so it may. A string the walk does not read (an expansion or a glob in it, or more than `NESTED_DEPTH` levels deep) may do anything, so the script is walked again with nothing trusted (`Walker.found`), as for arithmetic that reads text not known. Each line of a string is a unit, as a line of the script: an expansion error discards the rest of the string's line, and the shell goes on after `eval` (bash 5.2: `eval 'echo $((1/0)); echo a'; echo b` prints `b`). An error that may happen there may also end the line of the `eval` (bash does so for an integer given text that is no expression, `declare -i n; eval 'n=1/0'`, and for a subscript that fails). The model of another shell (`bash -c S`) decides from the values that reach it, so it trusts a variable only when the outer model does too (`withinTrust`), and nothing when the outer model trusts nothing or the shell reads startup files (`-l`, `-i`, an exported `BASH_ENV`).
**Strings run as scripts (4.13).** The walk runs a static `eval` string where `eval` runs, so its conditions are decided and its dead code said as elsewhere. It runs a trap's action where `trap` sets it (only for what it changes) and after the top level, but decides nothing in it and says none of it is dead: a signal may run it at any point, and an EXIT trap runs not only at the end or at `exit`, but also at `${u:?}`, at a `set -u` error and at a signal, with values the walk does not see there. The trust scan reads the same trees as the walk (the model's `Scripts`, `StringTrees`: each string is parsed once), as foreign text (a trap's action may run at any time); so what a statement of a string has that the walk does not run (subscripts, offsets that may fail, names read as arithmetic) is found as for the script's own statements, with what the whole script declares. So are the trees of the aliases the walk expands (an alias's text with the rest of the command, `StringTrees.expansion`), read at each command named like an alias the script defines: the text as foreign text, the rest of the command where the command is. A trap's action that may leave what a signal interrupts, otherwise than by `exit`, makes the script trust nothing: an error that may end the line around it (bash: `declare -i q; trap "q='1+'" USR1` discards the rest of the line `kill -USR1 $$` is on), or a `return`, `break` or `continue` that its own functions and loops do not hold (`trap return USR1` returns from the function the signal interrupts). An EXIT trap's action ends with the shell, so it may. A string the walk does not read (an expansion or a glob in it, or more than `NESTED_DEPTH` levels deep) may do anything, so the script is walked again with nothing trusted (`Walker.found`), as for arithmetic that reads text not known. Each line of a string is a unit, as a line of the script: an expansion error discards the rest of the string's line, and the shell goes on after `eval` (bash 5.2: `eval 'echo $((1/0)); echo a'; echo b` prints `b`). An error that may happen there may also end the line of the `eval` (bash does so for an integer given text that is no expression, `declare -i n; eval 'n=1/0'`, and for a subscript that fails). The model of another shell (`bash -c S`) decides from the values that reach it, so it trusts a variable only when the outer model does too (`withinTrust`), and nothing when the outer model trusts nothing or the shell reads startup files (`-l`, `-i`, an exported `BASH_ENV`).

**Visible effects.** The state after a decided branch (variables, options, functions, aliases: what B2 to B5 read). `StateModel.condition(n)` gives, for an `if`, `elif`, loop, `&&`/`||` list or `case`, the decision every walk made and the variables it read (with their values). The analyzer adds a note (not a pitfall: a value set at the top of a script, `DEBUG=0`, is a common way to switch code off) when the decision reads a variable or `$?`; a condition of literals only (`while true`, `if false`) says nothing, as its meaning is plain:

Expand Down
16 changes: 16 additions & 0 deletions src/semantics/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ export function commandName(c: SyntaxNode): string | undefined {
return staticWordValue(field(c, 'name')!.namedChildren[0]);
}

/** The name of the simple command `c` when bash may read it as an alias: a plain word, with no quote or expansion. */
export function aliasName(c: SyntaxNode): string | undefined {
const word = field(c, 'name')!.namedChildren[0];
const name = commandName(c);
return word.type === 'word' && word.text === name ? name : undefined;
}

/** `alias name=value` (or `'name=value'`): the name and what it stands for (static, or the text without its outer quotes). */
export function aliasOf(a: SyntaxNode): { name: string; value: string } | undefined {
const v = staticWordValue(a);
const text = v ?? a.text;
const m = /^([\w.:@%+-]+)=(['"]?)([\s\S]*)\2$/.exec(text);
if (!m) return undefined;
return { name: m[1], value: v !== undefined ? v.slice(v.indexOf('=') + 1) : m[3] };
}

/** The words of a builtin read with its spec (or `spec`: a function of the host, bats' `run`). */
export function builtinParse(name: string, args: readonly SyntaxNode[], spec = builtins[name]): ArgvParse {
return interpret(spec, args.map(argWord), { name });
Expand Down
Loading
Loading