Skip to content

Bash extractor: bash x.sh and ./x.sh produce no cross-file edge (only source/. do) #1756

Description

@Stashub

Summary

extract_bash creates a cross-file edge only when a command is source or .. The two most common ways one shell script runs another — bash other.sh and ./other.sh — produce no edge at all. In any repo where scripts invoke each other by execution rather than sourcing, every script ends up as an isolated pair of nodes (the file node plus its <name> script entrypoint), and the call topology is missing from the graph.

Version

Reproduced on 0.9.11 (also present in 0.9.7). Python 3.14.

Reproduction

Four files in an empty directory:

# b.sh
#!/bin/bash
echo hi

# exec_call.sh
#!/bin/bash
bash ./b.sh

# bare_call.sh
#!/bin/bash
./b.sh

# source_call.sh
#!/bin/bash
source ./b.sh
from pathlib import Path
from graphify.extractors.bash import extract_bash

for f in ["exec_call.sh", "bare_call.sh", "source_call.sh"]:
    r = extract_bash(Path(f))
    rels = sorted({e.get("relation") or e.get("type") for e in r["edges"]})
    print(f, rels)

Actual

exec_call.sh     ['contains']
bare_call.sh     ['contains']
source_call.sh   ['contains', 'imports_from']

Expected

exec_call.sh and bare_call.sh should each yield a cross-file edge to b.sh — something like invokes, alongside the existing imports_from for source.

Cause

graphify/extractors/bash.py:64

_BASH_SOURCE_COMMANDS = frozenset({"source", "."})

The command branch (~line 164) checks cmd in _BASH_SOURCE_COMMANDS and returns otherwise. An exec-position script path is never considered.

Suggested fix

In the same command branch, additionally treat as an invocation:

  1. a command_name whose literal ends in .sh (covers ./b.sh, /abs/path/b.sh);
  2. the first word argument of bash, sh, zsh, ksh, dash (covers bash ./b.sh).

Resolve the path the same way source already does, and keep the existing resolved.exists() guard — it preserves the traversal protection noted in the B-1 comment, since an edge is only emitted for a file that is actually present in the tree.

Paths that cannot be resolved on disk are out of scope here; skipping them silently, as source already does, is fine.

Impact

Any shell-heavy repository. source is the minority idiom for invoking a sibling script; bash x.sh and ./x.sh are the common ones, and today both are invisible to the graph.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions