Skip to content

Commit

Permalink
Path release 0.8.2
Browse files Browse the repository at this point in the history
Signed-off-by: Torsten Long <[email protected]>
  • Loading branch information
razziel89 committed Apr 3, 2024
1 parent 5882824 commit d913b84
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 8 deletions.
55 changes: 47 additions & 8 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,42 +562,81 @@ shellmock commands <<< "if command -v git; then git pull; else hg pull; fi"
```

**Example**:
Some cases that cannot be found
Some cases that cannot be found by default

```bash
# Define two functions first.
# Put the following function definitions in a file `script.sh`.
func1() {
echo "Running func1."
}
func2() {
local ls=ls
func1
# Output all files in a directory using `cat`.
find . -type f | xargs cat
# Output all files in a directory using `cat` via their full paths.
find . -type f | xargs readlink -f | xargs cat
# Calling "ls" but with its name stored in a variable.
"${ls}"
func1
}

shellmock commands -f -c <<< "$(type func2 | tail -n+2)"
# Run this in the directory containing `script.sh`.
shellmock commands -f -c < script.sh
# Output will be:
# find:1
# func1:2
# xargs:1
# xargs:2
```

Note how the `-c` flag causes the number of occurrences to be reported.
Furthermore, the `-f` flag causes `func1` to be reported, too, even though it is
a known shell function.

Note how `cat` is not detected because it is not called directly by the script.
Instead, it is called indirectly via `xargs`.
Note how neither `cat` nor `readlink` are detected because they are not called
directly by the script.
Instead, they are being called indirectly via `xargs`.
Also note how `ls` is not detected even though it is called directly by the
script.
However, its name is stored in a shell variable.
To be able to detect such cases, the values of all shell variables would have to
be known, which is not possible without executing the script.

To support examples like the one above, `shellmock` allows for specifying
commands that are used indirectly by adding specific directives as comments.
Lines containing directives generally look like `# shellmock:
uses-command=cmd1,cmd2` and may be followed by a comment.
The above example can thus be updated to report all used executables.

**Example**:
Using directives to specify used executables

```bash
# Put the following function definitions in a file `script.sh`.
func1() {
echo "Running func1."
}
func2() {
local ls=ls
func1
# Output all files in a directory using `cat` via their full paths. This calls
# `cat` and `basename` via `xargs`.
# shellmock: uses-command=readlink,cat
find . -type f | xargs readlink -f | xargs cat
# shellmock: uses-command=ls # Calling "ls" with its name in a variable.
"${ls}"
func1
}

# Run this in the directory containing `script.sh`.
shellmock commands -f -c < script.sh
# Output will be:
# cat:1
# find:1
# func1:2
# ls:1
# readlink:1
# xargs:2
```

### global-config

<!-- shellmock-helptext-start -->
Expand Down
58 changes: 58 additions & 0 deletions go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ import (
"log"
"os"
"slices"
"strings"
"unicode"

shell "mvdan.cc/sh/v3/syntax"
)

const (
commentChar = "#"
directiveSep = ":"
directiveStart = "shellmock"
usesCmdDirective = "uses-command="
)

func sortedKeys(data map[string]int) []string {
result := make([]string, 0, len(data))
for key := range data {
Expand Down Expand Up @@ -64,6 +73,50 @@ func findCommands(shellCode shell.Node) map[string]int {
return result
}

func findCommandsFromDirectives(shellCode string) map[string]int {
result := map[string]int{}
for lineIdx, orgLine := range strings.Split(shellCode, "\n") {
line := orgLine
isDirectiveLine := true
// First, detect comment lines. Then, detect lines with shellmock directives. Then, detect
// lines with the expected directive. Skip if any of the preconditions are not fulfilled.
for idx, prefix := range []string{commentChar, directiveStart, usesCmdDirective} {
line = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), directiveSep))
if !strings.HasPrefix(line, prefix) {
if idx >= 2 {
log.Printf(
"WARNING: found unknown shellmock directive in line %d: %s",
lineIdx+1, orgLine,
)
}
isDirectiveLine = false
break
}
line = strings.TrimPrefix(line, prefix)
}
if !isDirectiveLine {
continue
}
for _, cmd := range strings.Split(line, ",") {
// Stop if after some whitespace there is something starting with a comment character.
// That way, users can still add comments following the directive and executables
// containing whitespace are supported. The only thing we do not support is adding
// executables this way whose names contain a comment character following some space. We
// also do not support adding executables this way whose names contain commas.
idx := strings.Index(cmd, commentChar)
if idx > 0 && unicode.IsSpace(rune(cmd[idx-1])) {
// Make sure to add the last command before the trailing comment.
result[strings.TrimSpace(cmd[:idx])]++
break
}
if len(cmd) != 0 {
result[cmd]++
}
}
}
return result
}

func main() {
content, err := io.ReadAll(bufio.NewReader(os.Stdin))
if err != nil {
Expand All @@ -74,6 +127,11 @@ func main() {
log.Fatalf("failed to parse shell code: %s", err.Error())
}
commands := findCommands(parsed)
// Also find commands that are noted by shellmock directives.
moreCommands := findCommandsFromDirectives(string(content))
for cmd, count := range moreCommands {
commands[cmd] += count
}
for _, cmd := range sortedKeys(commands) {
count := commands[cmd]
fmt.Printf("%s:%d\n", cmd, count)
Expand Down
1 change: 1 addition & 0 deletions lib/mock_management.bash
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ __shellmock__assert() {
declare -a actual_argspecs
mapfile -t actual_argspecs < <(
if [[ -d "${__SHELLMOCK_OUTPUT}/${cmd_b32}" ]]; then
# shellmock: uses-command=cat
find "${__SHELLMOCK_OUTPUT}/${cmd_b32}" -mindepth 2 -type f \
-name argspec -print0 | xargs -r -0 cat | sort -u
fi
Expand Down
1 change: 1 addition & 0 deletions lib/shellmock.bash
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ __shellmock_internal_trap() {
then
local defined_cmds
readarray -d $'\n' -t defined_cmds < <(
# shellmock: uses-command=basename
find "${__SHELLMOCK_MOCKBIN}" -type f -print0 | xargs -r -0 -I{} basename {}
) && wait $!

Expand Down
24 changes: 24 additions & 0 deletions tests/extended.bats
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,27 @@ EOF
)
[[ ${output} == $(_join $'\n' "${exes[@]}") ]]
}

@test "hinting at which executables are being used" {
# We support multiple ways to specify directives. Test that they all work.
directives=(
'# shellmock uses-command=cmd1'
'#shellmock:uses-command=cmd1,cmd with spaces,cmd2 # followed by a comment'
' # shellmock: uses-command=cmd2,cmd2'
)
run -0 shellmock commands -c <<< "$(_join $'\n' "${directives[@]}")"

exes=(
"cmd with spaces:1"
"cmd1:2"
"cmd2:3"
)
[[ ${output} == $(_join $'\n' "${exes[@]}") ]]
}

@test "warning about unknown directives" {
line='# shellmock: unknown-directive=value'
script=$'\n\n\n'"${line}"$'\n\n'
run -0 shellmock commands -c <<< "${script}"
[[ ${output} == *"WARNING: found unknown shellmock directive in line 4: ${line}"* ]]
}

0 comments on commit d913b84

Please sign in to comment.