diff --git a/README.md b/README.md
index 951820a..544a885 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,18 @@
## Overview
-This Bash script interactively checks specific file or directory permissions (Owner, Group, or Other; Read, Write, or Execute) for a list of paths provided in an input file. It provides clear, color-coded output indicating whether the specified permission is set for each path.
+This Bash script checks specific file or directory permissions (Owner, Group, or Other; Read, Write, or Execute) for a list of paths provided in an input file. It provides clear, color-coded output indicating whether the specified permission is set for each path.
+
+It greets you with a big title banner and a short description, then runs either
+**interactively** — prompting for the input file and the permission to check —
+or **non-interactively** via command-line flags, which makes it easy to drop
+into scripts and CI. When any required value is omitted and a terminal is
+attached, the script prompts for it; when nothing is attached (a pipe or CI
+job), it fails fast with a clear message instead of hanging.
+
+
+
+
## Use Case
@@ -23,27 +34,38 @@ This script is useful for:
## Features
+* **Title banner:** prints a big ASCII title and a one-line description each run.
+* **Two run modes:** fully interactive prompts, or non-interactive with flags
+ (`--file`, `--who`, `--perm`) for scripting and CI.
* Checks permissions for files and directories listed in a specified input file.
-* Allows checking for Owner (`u`), Group (`g`), or Other (`o`) permissions.
+* Allows checking for Owner (`u`), Group (`g`), or Other (`o`) permissions, or
+ **all three at once** with `--all` (a compact permission matrix).
* Allows checking for Read (`r`), Write (`w`), or Execute (`x`) permissions.
-* Interactive prompts guide the user to select the input file and the desired permission check.
-* Input validation for file existence and permission choices.
+* Interactive prompts guide the user to select the input file and the desired
+ permission check, and validate every choice.
* Clear, color-coded output:
* **Green (`YES`)**: The specified permission is set.
* **Red (`NO`)**: The specified permission is **not** set.
-* Displays the full permission string (e.g., `Owner: rwx, Group: r-x, Other: r--`) for context in the output.
+* A **summary line** at the end with granted / denied / skipped counts.
+* Displays the full permission string (e.g., `owner rwx | group r-x | other r--`)
+ for context in the output.
+* **Comment & blank-line support:** lines in the input file that are empty or
+ start with `#` are skipped.
* Gracefully handles and reports paths listed in the input file that do not exist.
-* Nicely formats long prompt text using line wrapping.
+* **Cross-platform:** works with both GNU (`stat -c`) and BSD/macOS (`stat -f`)
+ `stat`, so no GNU coreutils install is required on macOS.
+* Color is disabled automatically when output is piped or redirected, and can be
+ turned off explicitly with `--no-color` (or the `NO_COLOR` environment variable).
+* `--help` and `--version` flags.
## Prerequisites
* A Bash-compatible shell (standard on most Linux distributions and macOS).
* Standard Unix/Linux command-line utilities, specifically:
- * GNU `stat` (used for retrieving file status, including permissions). The
- script calls `stat -c '%A'`, which is GNU coreutils syntax — standard on
- Linux. On macOS/BSD, install GNU coreutils and make sure its `stat` comes
- first on your `PATH` (for Homebrew, the `gnubin` directory).
- * `read`, `echo`, `printf` (standard shell built-ins).
+ * `stat` for retrieving file permissions. The script auto-detects GNU
+ (`stat -c '%A'`) and BSD/macOS (`stat -f '%Sp'`) variants, so it works on
+ Linux and macOS out of the box — no GNU coreutils install required.
+ * `read`, `printf`, `tr` (standard shell built-ins / utilities).
## Installation
@@ -55,33 +77,71 @@ This script is useful for:
## How to Run
-1. Create an input file containing the list of absolute paths you want to check (see [Input File Format](#input-file-format) below).
-2. Open your terminal.
-3. Navigate to the directory where you saved `dirPathPerms.sh`.
-4. Execute the script:
- ```bash
- ./dirPathPerms.sh
- ```
-5. Follow the interactive prompts:
- * Enter the path to your input file when prompted.
- * Choose whether to check permissions for Owner (`O`), Group (`G`), or Other (`E`).
- * Choose whether to check for Read (`R`), Write (`W`), or Execute (`X`) permission.
+Create an input file containing the list of absolute paths you want to check
+(see [Input File Format](#input-file-format) below), then run the script in
+whichever mode suits you.
+
+### Interactive
+
+```bash
+./dirPathPerms.sh
+```
-The script will then process each path in your input file and print the results to the console.
+Follow the prompts:
+
+* Enter the path to your input file when prompted.
+* Choose whether to check permissions for Owner (`O`), Group (`G`), Other (`E`),
+ or All (`A`).
+* Choose whether to check for Read (`R`), Write (`W`), or Execute (`X`) permission.
+
+The script processes each path and prints the results to the console.
+
+### Non-interactive
+
+Supply the values as flags and the script runs without prompting — ideal for
+scripts, cron jobs, and CI:
+
+```bash
+# Does the group have write access to every listed path?
+./dirPathPerms.sh --file paths.txt --who group --perm write
+
+# Show read access for owner/group/other across all paths, colors off
+./dirPathPerms.sh --all --perm read --file paths.txt --no-color
+
+# The file can also be passed positionally
+./dirPathPerms.sh -w owner -p x paths.txt
+```
+
+If some (but not all) values are provided, the script prompts for the rest when
+a terminal is attached, or exits with a helpful error when one is not.
+
+### Command-line options
+
+| Option | Description |
+|---|---|
+| `-f`, `--file FILE` | Input file: one absolute path per line. Blank lines and `#` comments are ignored. |
+| `-w`, `--who WHO` | Whose permission to check: `owner`\|`group`\|`other` (aliases `u`\|`g`\|`o`). |
+| `-p`, `--perm PERM` | Permission to check: `read`\|`write`\|`execute` (aliases `r`\|`w`\|`x`). |
+| `-a`, `--all` | Check owner, group **and** other at once (permission matrix). |
+| `--no-color` | Disable colored output (also honors the `NO_COLOR` env var). |
+| `-h`, `--help` | Show help and exit. |
+| `-V`, `--version` | Print the version and exit. |
## Input File Format
The input file should be a plain text file where **each line contains exactly one absolute path** to a file or directory.
* **Absolute paths are required** to ensure the script can find the files/directories regardless of where the script itself is executed from.
-* Lines starting with `#` could be used for comments if you modify the script to ignore them, but the current version treats every line as a potential path.
+* **Blank lines and lines starting with `#` are ignored**, so you can annotate and space out the file freely.
**Example Input File (`myPaths.txt`):**
```text
+# system files
/etc/passwd
/home/user/important_script.sh
/var/log/app.log
+
/tmp
/non/existent/path
/data/shared_folder
@@ -100,58 +160,39 @@ Let's say you want to check if members of the owning **Group** have **Write** ac
* `/non/existent/path` : Does not exist
* `/data/shared_folder` : `drwxrwx---` (Owner: rwx, Group: rwx, Other: ---)
-### Running the Script
+### Running the Script (non-interactive)
```bash
-./dirPathPerms.sh
-```
-
-### Interaction
-
-```text
-Example file format (one absolute path per line):
-
-/location/of/dirname1
-/location/of/dirname2
-/location/of/filename1
-/location/of/filename2
-
-Enter the name of the file containing the directories or files we should check.
-Absolute paths are required if the script is executed from a different
-location.
-
-File path: my_paths.txt # <-- User enters the file path
-
-Check permissions for (O)wner, (G)roup, Oth(e)r?
-
-Choice: G # <-- User enters 'G' for Group
-
-Check for (R)ead, (W)rite, or e(X)ecute permissions?
-
-Permission: W # <-- User enters 'W' for Write
+./dirPathPerms.sh --file myPaths.txt --who group --perm write
```
### Expected Output
```text
+Checking write permission for Group — from myPaths.txt
-# (Output color formatting shown conceptually with Markdown)
-# **Green Text** for YES, *Red Text* for NO
+ /etc/passwd NO (no write for Group) [owner rw- | group r-- | other r--]
+ /home/user/important_script.sh NO (no write for Group) [owner rwx | group r-x | other ---]
+ /var/log/app.log YES [owner rw- | group rw- | other ---]
+ /tmp YES [owner rwx | group rwx | other rwt]
+ /non/existent/path SKIP (path does not exist)
+ /data/shared_folder YES [owner rwx | group rwx | other ---]
-**/etc/passwd *NO write permission for Group (Owner: rw-, Group: r--, Other: r--)*
-**/home/user/important_script.sh *NO write permission for Group (Owner: rwx, Group: r-x, Other: ---)*
-**/var/log/app.log **YES (Owner: rw-, Group: rw-, Other: ---)**
-**/tmp **YES (Owner: rwx, Group: rwx, Other: rwt)**
-Skipping: /non/existent/path (Not a file or directory)
-**/data/shared_folder **YES (Owner: rwx, Group: rwx, Other: ---)**
+Summary: 3 granted, 2 denied, 1 skipped (5 checked).
```
+`YES` lines are green, `NO` lines red, and `SKIP` lines yellow (colors are
+omitted when output is piped or `--no-color` is set). With `--all`, each path
+instead shows a per-class matrix, e.g. `u+ g- o-` for a permission present for
+the owner but not the group or other.
+
## Output Explanation
-* `YES` (Green Text): Indicates that the requested permission (`write` in the example) is set for the specified entity (`Group` in the example) on that file or directory.
-* `NO` (Red Text): Indicates that the requested permission is not set for the specified entity. The message clarifies which permission (`write`) and entity (`Group`) were checked.
-* (Owner: ..., Group: ..., Other: ...): Appears on both `YES` and `NO` lines, showing the actual permission breakdown (`rwx` format) for Owner, Group, and Other for context.
-* `Skipping: ... (Not a file or directory)`: This message is printed when a path listed in the input file does not exist on the filesystem.
+* `YES` (Green): The requested permission (`write` in the example) is set for the specified entity (`Group` in the example) on that path.
+* `NO` (Red): The requested permission is not set for the specified entity. The message clarifies which permission and entity were checked.
+* `[owner ... | group ... | other ...]`: Appears on both `YES` and `NO` lines, showing the actual permission breakdown (`rwx` format) for context.
+* `SKIP ...` (Yellow): Printed when a path listed in the input file does not exist or its mode can't be read.
+* `Summary`: A final tally of granted / denied / skipped paths.
## License
diff --git a/assets/dirPathPerms_appThumbnail.png b/assets/dirPathPerms_appThumbnail.png
new file mode 100644
index 0000000..73358e5
Binary files /dev/null and b/assets/dirPathPerms_appThumbnail.png differ
diff --git a/dirPathPerms.sh b/dirPathPerms.sh
index 83954ec..a6adcaa 100755
--- a/dirPathPerms.sh
+++ b/dirPathPerms.sh
@@ -1,123 +1,347 @@
-#!/bin/bash
+#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
+#
+## Author: Tito Valentin
+## Name of Program: dirPathPerms.sh
+## Date Created: 2026-07-13
+## Description: Interactive and non-interactive checker that reports whether a
+## chosen permission (read/write/execute) is set for the owner,
+## group, or other on every path listed in an input file.
-# Function to wrap long lines
-fold() {
- local c word wrapped
- while read -r line; do
+set -uo pipefail
+
+VERSION="0.1.0"
+
+# ---------------------------------------------------------------------------
+# Colors. Disabled when --no-color / $NO_COLOR is set, or stdout is not a TTY
+# (so redirected/piped output stays clean and free of escape codes).
+# ---------------------------------------------------------------------------
+USE_COLOR="auto"
+setup_colors() {
+ if [[ "$USE_COLOR" == "no" || -n "${NO_COLOR:-}" || ! -t 1 ]]; then
+ BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" MAGENTA="" RESET=""
+ else
+ BOLD=$'\e[1m' DIM=$'\e[2m'
+ RED=$'\e[1;31m' GREEN=$'\e[1;32m' YELLOW=$'\e[1;33m'
+ MAGENTA=$'\e[1;35m' CYAN=$'\e[1;36m'
+ RESET=$'\e[0m'
+ fi
+}
+
+# All banner/prompt/summary "chrome" goes to stderr so that stdout carries
+# only the per-path results and stays pipe-friendly.
+emsg() { printf '%s\n' "$*" >&2; }
+
+# ---------------------------------------------------------------------------
+# Big, friendly title with a description underneath it.
+# ---------------------------------------------------------------------------
+print_banner() {
+ local rule
+ rule=$(printf '%.0s━' {1..64})
+ {
+ printf '\n%s%s%s\n' "$CYAN" "$rule" "$RESET"
+ printf '%s%s\n' "$BOLD$MAGENTA" "$RESET"
+ # figlet "standard" rendering of "dirPathPerms" (embedded; no runtime dep)
+ printf '%s' "$BOLD$CYAN"
+ cat <<'BANNER'
+ _ _ ____ _ _ ____
+ __| (_)_ __| _ \ __ _| |_| |__ | _ \ ___ _ __ _ __ ___ ___
+ / _` | | '__| |_) / _` | __| '_ \| |_) / _ \ '__| '_ ` _ \/ __|
+| (_| | | | | __/ (_| | |_| | | | __/ __/ | | | | | | \__ \
+ \__,_|_|_| |_| \__,_|\__|_| |_|_| \___|_| |_| |_| |_|___/
+BANNER
+ printf '%s' "$RESET"
+ printf ' %sFile & Directory Permission Checker%s %sv%s%s\n' \
+ "$BOLD" "$RESET" "$DIM" "$VERSION" "$RESET"
+ printf ' %sAudit whether read / write / execute is set for owner, group, or other.%s\n' \
+ "$DIM" "$RESET"
+ printf '%s%s%s\n\n' "$CYAN" "$rule" "$RESET"
+ } >&2
+}
+
+usage() {
+ cat >&2 </dev/null || stat -f '%Sp' "$1" 2>/dev/null
+}
+
+# ---------------------------------------------------------------------------
+# Input normalization / validation.
+# ---------------------------------------------------------------------------
+who="" # u | g | o | all
+who_text=""
+normalize_who() {
+ case "$(lc "$1")" in
+ owner|u|user) who="u"; who_text="Owner" ;;
+ group|g) who="g"; who_text="Group" ;;
+ other|o) who="o"; who_text="Other" ;;
+ all|a) who="all"; who_text="All" ;;
+ *) return 1 ;;
+ esac
+}
+
+perm="" # r | w | x
+perm_text=""
+normalize_perm() {
+ case "$(lc "$1")" in
+ read|r) perm="r"; perm_text="read" ;;
+ write|w) perm="w"; perm_text="write" ;;
+ execute|exec|x) perm="x"; perm_text="execute" ;;
+ *) return 1 ;;
+ esac
+}
+# ---------------------------------------------------------------------------
+# Interactive prompts (used only for values not supplied on the command line).
+# ---------------------------------------------------------------------------
+prompt_file() {
+ emsg ""
+ emsg "Example file format (one absolute path per line):"
+ emsg ""
+ emsg " /location/of/dirname1"
+ emsg " /location/of/filename1"
+ emsg ""
+ wrap_text >&2 <<< "Enter the path to the file listing the directories or files to check. Absolute paths are required if the script runs from another location."
while true; do
+ printf '%s' "${BOLD}File path:${RESET} " >&2
+ read -r file_path
if [[ -f "$file_path" ]]; then
break
- else
- echo
- echo "Error: File not found. Please try again."
- echo -n "File path: " # Added -n to prevent new line
- read -r file_path
fi
+ emsg ""
+ emsg "${RED}Error: file not found.${RESET} Please try again."
done
}
-# Function to get and validate user input for permissions
-get_permissions() {
+prompt_who() {
while true; do
- echo ""
- fold <<< "Check permissions for (O)wner, (G)roup, Oth(e)r? "
- echo ""
- echo -n "Choice: " # Added -n to prevent new line
- read -r object
- case "$object" in
- [Oo]) object="u"; object_text="Owner"; break;;
- [Gg]) object="g"; object_text="Group"; break;;
- [Ee]) object="o"; object_text="Other"; break;;
- *) echo "Invalid choice. Please enter O, G, or E.";;
- esac
+ emsg ""
+ printf '%sCheck permissions for (O)wner, (G)roup, Oth(e)r, or (A)ll?%s ' \
+ "$BOLD" "$RESET" >&2
+ read -r reply
+ if normalize_who "$reply"; then
+ break
+ fi
+ emsg "${RED}Invalid choice.${RESET} Enter O, G, E, or A."
done
+}
- echo # Newline added here for separation
-
+prompt_perm() {
while true; do
- fold <<< "Check for (R)ead, (W)rite, or e(X)ecute permissions? "
- echo ""
- echo -n "Permission: " # Added -n to prevent new line
- read -r permission
- case "$permission" in
- [Rr]) permission="r"; perm_text="read"; break;;
- [Ww]) permission="w"; perm_text="write"; break;;
- [Xx]) permission="x"; perm_text="execute"; break;;
- *) echo "Invalid choice. Please enter R, W, or X.";;
- esac
+ emsg ""
+ printf '%sCheck for (R)ead, (W)rite, or e(X)ecute permission?%s ' \
+ "$BOLD" "$RESET" >&2
+ read -r reply
+ if normalize_perm "$reply"; then
+ break
+ fi
+ emsg "${RED}Invalid choice.${RESET} Enter R, W, or X."
done
}
-# Get file path from user
-get_file
+# ---------------------------------------------------------------------------
+# Does the 3-char class field (e.g. "rwx" or "r-x") grant $perm?
+# ---------------------------------------------------------------------------
+has_perm() { [[ "$1" == *"$perm"* ]]; }
+
+# ---------------------------------------------------------------------------
+# The check loop.
+# ---------------------------------------------------------------------------
+run_checks() {
+ local granted=0 denied=0 skipped=0 total=0
+ local line path perms owner_perm group_perm other_perm current
+ local badge_u badge_g badge_o
+
+ emsg "${BOLD}Checking ${perm_text} permission for ${who_text}${RESET} — from ${file_path}"
+ emsg ""
-# Get permission details from user
-get_permissions
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ # Skip blanks and comments.
+ case "$line" in
+ '' | '#'*) continue ;;
+ esac
+ path="$line"
+
+ if [[ ! -e "$path" ]]; then
+ printf '%s %-44s SKIP (path does not exist)%s\n' \
+ "$YELLOW" "$path" "$RESET"
+ skipped=$((skipped + 1))
+ continue
+ fi
-echo
+ perms=$(mode_string "$path")
+ if [[ -z "$perms" ]]; then
+ printf '%s %-44s SKIP (could not read mode)%s\n' \
+ "$YELLOW" "$path" "$RESET"
+ skipped=$((skipped + 1))
+ continue
+ fi
-# Check permissions for each path in the file
-while IFS= read -r path; do
- if [[ -e "$path" ]]; then
- # Get permissions for each object
- perms=$(stat -c '%A' "$path")
owner_perm=${perms:1:3}
group_perm=${perms:4:3}
other_perm=${perms:7:3}
+ total=$((total + 1))
- # Check if permission is set for the specified object
- case "$object" in
- "u")
- current_perm=$owner_perm
- ;;
- "g")
- current_perm=$group_perm
- ;;
- "o")
- current_perm=$other_perm
- ;;
+ if [[ "$who" == "all" ]]; then
+ # Permission matrix: show the requested permission per class.
+ if has_perm "$owner_perm"; then badge_u="${GREEN}u+${RESET}"; granted=$((granted + 1)); else badge_u="${RED}u-${RESET}"; fi
+ if has_perm "$group_perm"; then badge_g="${GREEN}g+${RESET}"; granted=$((granted + 1)); else badge_g="${RED}g-${RESET}"; fi
+ if has_perm "$other_perm"; then badge_o="${GREEN}o+${RESET}"; granted=$((granted + 1)); else badge_o="${RED}o-${RESET}"; fi
+ printf ' %-44s %s %s %s %s[%s %s %s]%s\n' \
+ "$path" "$badge_u" "$badge_g" "$badge_o" \
+ "$DIM" "$owner_perm" "$group_perm" "$other_perm" "$RESET"
+ continue
+ fi
+
+ case "$who" in
+ u) current=$owner_perm ;;
+ g) current=$group_perm ;;
+ o) current=$other_perm ;;
esac
- if [[ $current_perm == *"$permission"* ]]; then
- printf "\e[1;32m%-40s YES (Owner: $owner_perm, Group: $group_perm, Other: $other_perm)\e[0m\n" "$path"
+ if has_perm "$current"; then
+ printf '%s %-44s YES%s %s[owner %s | group %s | other %s]%s\n' \
+ "$GREEN" "$path" "$RESET" \
+ "$DIM" "$owner_perm" "$group_perm" "$other_perm" "$RESET"
+ granted=$((granted + 1))
else
- printf "\e[1;31m%-40s NO $perm_text permission for $object_text (Owner: $owner_perm, Group: $group_perm, Other: $other_perm)\e[0m\n" "$path"
+ printf '%s %-44s NO (no %s for %s)%s %s[owner %s | group %s | other %s]%s\n' \
+ "$RED" "$path" "$perm_text" "$who_text" "$RESET" \
+ "$DIM" "$owner_perm" "$group_perm" "$other_perm" "$RESET"
+ denied=$((denied + 1))
fi
+ done < "$file_path"
+
+ emsg ""
+ if [[ "$who" == "all" ]]; then
+ emsg "${BOLD}Summary:${RESET} ${total} path(s) checked, ${skipped} skipped — ${GREEN}${granted}${RESET} class-grants of ${perm_text}."
else
- echo "Skipping: $path (Not a file or directory)"
+ emsg "${BOLD}Summary:${RESET} ${GREEN}${granted} granted${RESET}, ${RED}${denied} denied${RESET}, ${YELLOW}${skipped} skipped${RESET} (${total} checked)."
fi
-done < "$file_path"
+}
+
+# ---------------------------------------------------------------------------
+# Argument parsing.
+# ---------------------------------------------------------------------------
+file_path=""
+main() {
+ local positional=""
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -f|--file)
+ [[ $# -ge 2 ]] || { setup_colors; emsg "Option $1 requires a value."; exit 2; }
+ file_path="$2"; shift 2 ;;
+ -w|--who)
+ [[ $# -ge 2 ]] || { setup_colors; emsg "Option $1 requires a value."; exit 2; }
+ if ! normalize_who "$2"; then setup_colors; emsg "Invalid --who value: $2 (use owner|group|other|all)."; exit 2; fi
+ shift 2 ;;
+ -p|--perm)
+ [[ $# -ge 2 ]] || { setup_colors; emsg "Option $1 requires a value."; exit 2; }
+ if ! normalize_perm "$2"; then setup_colors; emsg "Invalid --perm value: $2 (use read|write|execute)."; exit 2; fi
+ shift 2 ;;
+ -a|--all) who="all"; who_text="All"; shift ;;
+ --no-color) USE_COLOR="no"; shift ;;
+ -h|--help) setup_colors; usage; exit 0 ;;
+ -V|--version) printf 'dirPathPerms %s\n' "$VERSION"; exit 0 ;;
+ --) shift; [[ $# -gt 0 ]] && positional="$1"; break ;;
+ -*) setup_colors; emsg "Unknown option: $1"; usage; exit 2 ;;
+ *)
+ if [[ -z "$positional" ]]; then positional="$1"; else
+ setup_colors; emsg "Unexpected argument: $1"; exit 2
+ fi
+ shift ;;
+ esac
+ done
+
+ [[ -z "$file_path" && -n "$positional" ]] && file_path="$positional"
+
+ setup_colors
+ print_banner
+
+ # Fill in whatever wasn't provided on the command line. Prompt only when a
+ # terminal is attached; otherwise fail clearly (non-interactive contract).
+ if [[ -z "$file_path" ]]; then
+ if [[ -t 0 ]]; then prompt_file
+ else emsg "${RED}No input file given.${RESET} Pass --file FILE (see --help)."; exit 2; fi
+ elif [[ ! -f "$file_path" ]]; then
+ emsg "${RED}Error: file not found:${RESET} $file_path"; exit 2
+ fi
+
+ if [[ -z "$who" ]]; then
+ if [[ -t 0 ]]; then prompt_who
+ else emsg "${RED}No target given.${RESET} Pass --who owner|group|other|all (see --help)."; exit 2; fi
+ fi
+
+ if [[ -z "$perm" ]]; then
+ if [[ -t 0 ]]; then prompt_perm
+ else emsg "${RED}No permission given.${RESET} Pass --perm read|write|execute (see --help)."; exit 2; fi
+ fi
+
+ run_checks
+}
+main "$@"