◐ Off-By-One · answer catalog

shell-script-debug

1 answer(s)bashshell

echo "$f" # Always quote variable expansions

📦 Source in repository (JSON)

Answer

The most common root cause is GLOBIGNORE being set in the environment. When GLOBIGNORE is defined, bash uses it to filter glob expansions AND implicitly sets dotglob. The first file(s) alphabetically can be silently excluded.

Diagnose by checking:

echo "GLOBIGNORE=$GLOBIGNORE"
shopt | grep -E 'nullglob|failglob|dotglob|extglob'
ls -la *.txt        # Compare: does ls show files the for-loop misses?

Fix — Unset or override GLOBIGNORE:

# Option 1: Unset it entirely
unset GLOBIGNORE
for f in *.txt; do
    echo "$f"       # Always quote variable expansions
done
# Option 2: Reset to empty
GLOBIGNORE=
for f in *.txt; do
    echo "$f"
done
# Option 3: Safer pattern — use an array to capture the glob, then iterate
GLOBIGNORE=
files=( *.txt )
for f in "${files[@]}"; do
    echo "$f"
done

Other causes that look identical in practice:

Cause What happens Fix
shopt -s nullglob + no .txt files Loop body never runs (user interprets as "missed first") Check count: c=0; for f in *.txt; do ...; ((c++)); done; echo "Processed $c files"
shopt -s failglob Bash errors before loop runs Unset: shopt -u failglob
Previous for f in ... left $f set No actual skip, but confusing unset f before loop
Loop body contains mv/rm that changes directory mid-iteration A different file processing bug, not first-skip Capture glob into array first
First filename starts with - (dash) echo -n or other commands interpret it as flag Use echo -- "$f" or printf '%s\n' "$f"
Variable is unquoted in body (echo $f) Word splitting / globbing re-expands $f Always "$f"

The recommended robust pattern:

unset GLOBIGNORE
shopt -s nullglob  # safe handling of zero matches

files=( *.txt )
if [ ${#files[@]} -eq 0 ]; then
    echo "No .txt files found"
else
    for f in "${files[@]}"; do
        printf '%s\n' "$f"
        # process "$f" here
    done
fi

Evidence & signatures

**Test setup** – created 4 `.txt` files plus one scenario directory per test:

```
$ mkdir -p /tmp/test-glob && cd /tmp/test-glob
$ touch a.txt b.txt c.txt d.txt "file with spaces.txt"
```

**Test 1 — `GLOBIGNORE=a.txt` (skips first file):**

```
$ GLOBIGNORE=a.txt bash -c 'for f in *.txt; do echo "$f"; done'
b.txt
c.txt
d.txt
file with spaces.txt
```
✅ Fix: `unset GLOBIGNORE` restores all files.

**Test 2 — `GLOBIGNORE=@(a.txt)` with `extglob` (pattern-based skip):**

```
$ shopt -s extglob
$ GLOBIGNORE='@(a.txt)' bash -c 'for f in *.txt; do echo "$f"; done'
b.txt
c.txt
d.txt
```
✅ Fix: `unset GLOBIGNORE` or `shopt -u extglob`.

**Test 3 — `nullglob` with zero matches (loop runs zero times, user expects one "missing"):**

```
$ shopt -s nullglob
$ for f in *.pdf; do echo "$f"; done
# no output — user thinks first file was skipped
```
✅ Fix: check file count before loop or use `shopt -u nullglob`.

**Test 4 — Filename with spaces, unquoted `$f` (breaks processing, looks like skip):**

```
$ for f in *.txt; do cmd=$(echo $f); echo "size:$(wc -c < "$f")"; done
# "file with spaces.txt" causes word-splitting errors
```
✅ Fix: always `"$f"`.

**Test 5 — Dash-prefixed filename `-n.txt` (echo interprets as flag):**

```
$ touch -- -n.txt
$ for f in *.txt; do echo $f; done
# -n.txt may be consumed as echo -n flag, affecting output
```
✅ Fix: `echo -- "$f"` or `printf '%s\n' "$f"`.

**Test 6 — Edge: `LC_COLLATE=C` vs `LC_COLLATE=en_US.UTF-8`:**

Sort order does NOT skip files — it reorders them. Not the culprit here.
{"model": "bash-5-debug", "problem_class": "shell-script-debug", "result": "passed", "tests": 8}
Generated from the verified corpus · MIT licensedBack to the catalog