feat: check paths directly, with tilde/variable expansion and prompt history - #7
Merged
Merged
Conversation
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.
7 tasks
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>
7 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
~/Documents,$HOME/x, a quoted path, a Finder drag-and-dropmy\ file.txt, an indented line, a CRLF line ending — was looked up as-is and reported aspath does not exist. 7 of 8 realistic spellings failed.-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
~/~userand$VAR/${VAR}. Trimming now happens before the blank/comment test, so a CRLF file no longer turns every blank line into a bogus path.--path/-P(repeatable) for the same thing non-interactively, plus bare-argument support.--filehanded a directory now says "is a directory, not a list of paths" and points at--path.HISTFILEis cleared so a run never writes back to the user's shell history.tests/run_tests.sh, wired into CI.Design notes for review
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.lit_$HOME.txtstill resolves. This makes the change strictly non-regressive."${arr[@]}"on an empty array errors underset -u;read -e -proutes its prompt to stderr (which this script wants); coloredread -eprompts need\001/\002markers or readline miscounts the cursor column and corrupts the line on recall.Related issue
DIR-2
Validation
tests/interactive_test.pyattaches the script to a pseudo-terminal and verifies recall by sending a literal\x1b[A. It is skipped whenpython3is unavailable.--path, and as a bare argument; list mode and--allmatrix mode still behave.Checklist
git ls-files '*.sh' | xargs shellcheckpassesgit ls-files '*.sh' | xargs -n1 bash -npasses