Skip to content

feat: check paths directly, with tilde/variable expansion and prompt history - #7

Merged
iamteedoh merged 1 commit into
mainfrom
DIR-2-macos-path-resolution
Jul 17, 2026
Merged

feat: check paths directly, with tilde/variable expansion and prompt history#7
iamteedoh merged 1 commit into
mainfrom
DIR-2-macos-path-resolution

Conversation

@iamteedoh

Copy link
Copy Markdown
Owner

What does this PR do?

Makes the script usable for its actual purpose: pointing it at a path and being told the permissions.

Two separate defects combined so that, on macOS, essentially nothing a user typed worked:

  1. Paths were used verbatim. Every entry other than a bare absolute path — ~/Documents, $HOME/x, a quoted path, a Finder drag-and-drop my\ file.txt, an indented line, a CRLF line ending — was looked up as-is and reported as path does not exist. 7 of 8 realistic spellings failed.
  2. The prompt rejected the thing people were trying to check. It only accepted a file listing paths, and validated with -f. Typing ~/Documents — a directory that plainly exists — was reported as "file not found", which sent people hunting for a nonexistent path problem.

Fixing (1) alone left the reported symptom unchanged, because (2) was the real blocker.

Changes

  • Normalize each entry before lookup: strip a CRLF tail and surrounding whitespace, remove one surrounding pair of quotes, resolve backslash escapes, expand a leading ~/~user and $VAR/${VAR}. Trimming now happens before the blank/comment test, so a CRLF file no longer turns every blank line into a bogus path.
  • Accept a path or a list: a regular file is read as a list (the original mode, unchanged); anything else that exists is checked directly. Adds --path/-P (repeatable) for the same thing non-interactively, plus bare-argument support.
  • Honest errors: --file handed a directory now says "is a directory, not a list of paths" and points at --path.
  • Readline prompt: arrow-key recall of earlier entries and full line editing. HISTFILE is cleared so a run never writes back to the user's shell history.
  • Test suite (new): tests/run_tests.sh, wired into CI.

Design notes for review

  • Expansion is hand-rolled, not eval. An input file must only ever name files, never run commands, so $(...) and backticks are left literal. Only well-formed identifiers are substituted. There are tests asserting command substitution stays inert.
  • A literal name always wins. If the normalized form does not exist but the raw line does, the raw line is used — a file genuinely named lit_$HOME.txt still resolves. This makes the change strictly non-regressive.
  • Three bash 3.2 behaviors were verified rather than assumed, each a silent bug otherwise: "${arr[@]}" on an empty array errors under set -u; read -e -p routes its prompt to stderr (which this script wants); colored read -e prompts need \001/\002 markers or readline miscounts the cursor column and corrupts the line on recall.

Related issue

DIR-2

Validation

git ls-files '*.sh' | xargs shellcheck          # clean
git ls-files '*.sh' | xargs -n1 bash -n         # clean
./tests/run_tests.sh                            # 33 passed, 0 failed
gitleaks git . --config .gitleaks.toml          # no leaks found
  • 33 tests pass, including 4 pty-driven tests. The prompt and its arrow-key history only exist with a real terminal, so they cannot be covered by piping input; tests/interactive_test.py attaches the script to a pseudo-terminal and verifies recall by sending a literal \x1b[A. It is skipped when python3 is unavailable.
  • The path-spelling tests are proven regressions: 14 of them fail against the pre-fix script; the ones that pass on both are the no-regression guards.
  • Driven end-to-end on macOS (bash 3.2): the reported scenario now resolves at the prompt, via --path, and as a bare argument; list mode and --all matrix mode still behave.

Checklist

  • git ls-files '*.sh' | xargs shellcheck passes
  • git ls-files '*.sh' | xargs -n1 bash -n passes
  • gitleaks reports no secrets in Git history
  • Scripts and programs include a GPL-3.0-or-later SPDX header
  • No secrets, tokens, credentials, or private infrastructure details are committed
  • Documentation is updated for user-visible or operational changes
  • The PR title follows Conventional Commits

Paths given to the script were used verbatim, so every spelling other than a
bare absolute path was reported as "path does not exist". Normalize each entry
first: strip a CRLF tail and surrounding whitespace, remove one pair of quotes,
resolve backslash escapes, and expand a leading ~ and any $VAR. Expansion is
hand-rolled rather than delegated to eval so an input file can only name files,
never run commands. A literal name still wins when it exists, so a file whose
name genuinely contains ~, $, a quote, or a backslash is unaffected.

The prompt also only accepted a file listing paths, while rejecting the path a
user actually wanted to check. Because it validated with -f, a directory that
plainly exists was reported as "file not found". Accept either form: a regular
file is read as a list, anything else that exists is checked directly. Add
--path for the same thing non-interactively, and say what is actually wrong
when --file is handed a directory.

Give the prompt readline editing with arrow-key recall of earlier entries, and
clear HISTFILE so a run never writes back to the user's shell history.

Add a test suite covering each spelling an input file can contain, the literal
fallback, and the guarantee that command substitution stays inert. The prompt
and its history need a tty, so they are covered by a pty harness.
@iamteedoh
iamteedoh merged commit c7f3389 into main Jul 17, 2026
3 checks passed
@iamteedoh
iamteedoh deleted the DIR-2-macos-path-resolution branch July 17, 2026 00:39
iamteedoh added a commit that referenced this pull request Jul 17, 2026
## What does this PR do?

Follow-on to #7. Once a path resolved, the tool was still awkward to
actually use: every single check meant relaunching the script and
retyping every answer, and the results were flat lines that got hard to
scan past a handful of paths.

### Changes

- **The session stays open.** After a result it asks for another path,
reusing the class and permission already chosen, until you press `q`.
Only the fully interactive run loops — supplying everything via flags
still checks once and exits, which is what keeps the tool usable from
cron and CI.
- **An `all` option for permissions**, matching the one the who prompt
already had. `--perm all` (or `a` at the prompt) checks read, write and
execute together; combined with `--all` it gives the full 3×3 matrix of
every permission against every class.
- **Results render as a table**: bordered, with headers, one row per
path, and alternating row shading so neighbouring rows stay distinct.
`YES`/`NO` remain green/red.

```
┌─────────────┬─────────────┬─────────────┬─────────────┬────────────┐
│             │    Owner    │    Group    │    Other    │            │
│ Path        │  R   W   X  │  R   W   X  │  R   W   X  │ Mode       │
├─────────────┼─────────────┼─────────────┼─────────────┼────────────┤
│ /etc/passwd │  ✓   ✓   ·  │  ✓   ·   ·  │  ✓   ·   ·  │ -rw-r--r-- │
└─────────────┴─────────────┴─────────────┴─────────────┴────────────┘
```

### A bug this surfaced

Driving the session interactively caught something worth calling out,
because it **refines a rule #7 had just documented**. #7 said "a regular
file is read as a list of paths" — so typing `/etc/passwd`, the single
most obvious thing anyone would permission-check, parsed its *contents*
as paths:

```
│ nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false │ — │ does not exist │
│ root:*:0:0:System Administrator:/var/root:/bin/sh          │ — │ does not exist │
```

A regular file is now only read as a list when it looks like one — its
first meaningful line being a path (`/`, `~`, `$`). `--file` still
forces the list reading unconditionally. The README documents the rule.

### Design notes for review

- **Checking and rendering are now separate.** Rows are collected into
arrays first so the table can size each column to the widest value it is
about to print.
- **Column widths use explicit display widths, not `${#s}`.** `${#s}`
counts *bytes* outside a UTF-8 locale, so an em dash would measure 3
columns instead of 1 and skew every border. Alignment is verified under
`LC_ALL=C`.
- **Zebra striping sets the background once per row and closes it at end
of line.** Anything colored inside a row returns to the default
foreground with a fg-only reset rather than a full reset, which would
drop the row's background partway along the line.

## Related issue

DIR-3

## Validation

```
git ls-files '*.sh' | xargs shellcheck          # clean
git ls-files '*.sh' | xargs -n1 bash -n         # clean
./tests/run_tests.sh                            # 47 passed, 0 failed
gitleaks git . --config .gitleaks.toml          # no leaks found
```

- **47 tests pass**, up from 33, including 11 pty-driven tests. The
session loop, the `q` exit, the table, and the row striping only exist
with a real terminal, so `tests/interactive_test.py` drives them through
a pseudo-terminal.
- Table assertions replace the `YES`/`NO` line-format assertions the
previous suite used, since this changes that output format.
- Driven end-to-end on macOS (bash 3.2): the session loops, reuses
who/perm, reports a bad path without exiting, and quits cleanly on `q`.
- Rebased onto `main` after #7 merged; main's post-squash tree was
identical to the pre-squash tree, so the replay was conflict-free.
Verified green on the new base.

## Checklist

- [x] `git ls-files '*.sh' | xargs shellcheck` passes
- [x] `git ls-files '*.sh' | xargs -n1 bash -n` passes
- [x] gitleaks reports no secrets in Git history
- [x] Scripts and programs include a GPL-3.0-or-later SPDX header
- [x] No secrets, tokens, credentials, or private infrastructure details
are committed
- [x] Documentation is updated for user-visible or operational changes
- [x] The PR title follows Conventional Commits
iamteedoh pushed a commit that referenced this pull request Jul 17, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.0](v0.2.0...v0.3.0)
(2026-07-17)


### Features

* check paths directly, with tilde/variable expansion and prompt history
([#7](#7))
([c7f3389](c7f3389))
* interactive session with tabular output
([#9](#9))
([e1d5e08](e1d5e08))


### Bug Fixes

* report the real version from --version
([#10](#10))
([0d87357](0d87357))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
iamteedoh added a commit that referenced this pull request Jul 17, 2026
## What does this PR do?

Three releases of behavior landed (#7, #9, #10) and parts of the README
still described the tool as it was before. This audits the whole file
against what v0.3.0 actually does.

### Documents tab completion

The prompt has completed paths ever since readline arrived alongside the
arrow-key history, and nothing said so — which means nobody would ever
try it. Adds a keys table under **Interactive**: `Tab`, `↑`/`↓`,
`Ctrl-A`/`E`/`W`/`U`/`R`, `q`.

The table records what was **verified over a pty**, not what readline is
assumed to do. Two claims were corrected during testing:

- An earlier draft said "press Tab twice to list the candidates". That
is false for `read -e` — it fills the common prefix and beeps without
listing. Re-tested on a 120×40 pty in case readline was suppressing the
list on a 0×0 terminal; it was not.
- The session's history never touches your shell history (`HISTFILE` is
cleared), so that is now stated rather than left to guesswork.

### Corrects what had gone stale

| Was | Now |
|---|---|
| Overview: prompts "for the input file and the permission to check" | a
session taking a path *or* a list file, looping until `q` |
| Feature bullet advertising `owner rwx \| group r-x \| other r--` |
that format no longer exists — replaced with the `Mode` column |
| "Interactive prompts guide the user to select the input file" |
predated `--path` |
| "prompts for the rest when a terminal is attached" | had drifted under
the **Paths vs. lists** heading during earlier edits, reading as though
it were about disambiguation — moved back |
| Prerequisites | now name the Bash 3.2 floor (what macOS ships), the
tty the session needs, and that `getent`/`dscl` are only for *another*
user's `~` |
| Installation: "save the script content to a file" | predated the repo
having releases — clone or download the release |

### Adds an Exit codes section

The README recommends the tool for cron and CI but never documented that
**a denied permission exits `0`** — only usage errors exit `2`. Anyone
writing `dirPathPerms.sh … || alert` would get silence forever. Now
documented, with a grep-based recipe for acting on the result.

## Related issue

DIR-5

## Validation

Documentation-only, so the value is in whether the claims are **true**.
Every runnable claim was executed rather than eyeballed:

- **All 16 documented flags** exercised (`-P`, `--path`, `-f`, `--file`,
`-w` × 4, `-p` × 5, `-a`/`--all`, `--no-color`, `-h`, `-V`, positional,
repeatable `-P`) — all pass.
- **Exit codes verified** against the new table: denied → `0`; missing
list file, unknown option, `--file`+`--path`, and no-tty-no-path → `2`.
- **The CI recipe was run**: it fires on a group-writable directory and
stays silent when nothing matches.
- **Tab completion driven over a pty** for `/etc/pass`, `/var/lo`,
`~/Doc`.
- All README internal anchors resolve.
- Repo gate unchanged and green: shellcheck, `bash -n`, 47/47 tests,
gitleaks.

## Notes for review

Two things deliberately **not** done here:

- **Symlink caveat.** Verifying a README example surfaced a real bug —
`/tmp` reports the symlink's mode (`lrwxr-xr-x`) instead of the target's
(`drwxrwxrwt`), so the tool answers "not group-writable" when it is.
Raised as **DIR-6**. Documenting it as a caveat would enshrine a bug
that should be fixed instead; say the word if you'd rather ship the
caveat.
- **The hero image** in `assets/` still shows `v0.1.0` and the old `File
path:` prompt — the exact UX #7 removed. It needs re-capturing from a
real terminal; there is no terminal-to-image tooling available to
regenerate it here.

`docs:` — README-only, so release-please will not bump the version for
it.

## Checklist

- [x] `git ls-files '*.sh' | xargs shellcheck` passes
- [x] `git ls-files '*.sh' | xargs -n1 bash -n` passes
- [x] gitleaks reports no secrets in Git history
- [x] Scripts and programs include a GPL-3.0-or-later SPDX header
- [x] No secrets, tokens, credentials, or private infrastructure details
are committed
- [x] Documentation is updated for user-visible or operational changes
- [x] The PR title follows Conventional Commits
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant