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. + +

+ dirPathPerms banner and permission-check output +

## 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 "$@"