Last active
May 23, 2026 23:41
-
-
Save kyle0r/53b322cc558e60ba121721ee57093e5c to your computer and use it in GitHub Desktop.
cygwin: open arbitrary file paths with pause
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
| *.sh text eol=lf |
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
| #!/bin/sh | |
| # POSIX sh script; checked with shellcheck | |
| # Original snippet captured here: | |
| #+ https://htks.digiflakes.com/2024/01/windows-no-easy-way-to-view-list-of.html | |
| set -eu # exit on errors or undefined vars | |
| CR=$(printf '\r') | |
| version='2026.21.1' | |
| # version convention: date +%G\.%V\.1 (YEAR.WEEK.RELEASE) | |
| #+ where 1 is incremented per release within the given week | |
| me="${0##*/}" # basename | |
| me="${me%.sh}" # strip .sh suffix | |
| print_version() { printf "%s version: %s\n" "$me" "$version"; } | |
| # complain to STDERR and exit with error | |
| die() { printf '%s\n' "$*" >&2; exit 2; } | |
| # define pause function | |
| # stdin is the path list during the loop, so prompts must use the controlling terminal. | |
| # shellcheck disable=SC2120 | |
| pause() { printf '%s' "$*" >/dev/tty; read -r _ </dev/tty; } | |
| ###################################################################### | |
| # START usage related code | |
| usage() { | |
| usage=$(cat <<USAGE | |
| NAME | |
| $me - loops over a list of file paths and opens them one at a time with cygstart | |
| SYNOPSIS | |
| $me <list_of_file_paths.txt> | |
| VERSION | |
| $(print_version) | |
| DESCRIPTION | |
| Windows does not have native functionality to open an arbitrary list of files. | |
| This script was created to address this gap, where one has a list of | |
| arbitrary file paths and no easy way to view them. | |
| This script is interactive and requires a controlling terminal for prompts. | |
| Input paths must be Unix-like, shell-visible file paths. | |
| This script graduates the following ad-hoc snippet to a proper script: | |
| https://htks.digiflakes.com/2024/01/windows-no-easy-way-to-view-list-of.html | |
| USAGE | |
| ) | |
| printf "%s" "$usage" | "$pager"; exit 0 | |
| } | |
| usage_with_prompt() { | |
| printf "\n%s\n" "PAGER: $pager will now run to display help/usage info." | |
| [ -r /dev/tty ] && pause | |
| usage | |
| } | |
| # use less as PAGER fallback if no env pager is defined | |
| # https://stackoverflow.com/a/28085062/490487 | |
| : "${PAGER:=less}" | |
| pager=$PAGER | |
| case "$pager" in | |
| "" ) | |
| pager="less" | |
| ;; | |
| *[[:space:]]* ) | |
| # Multi-word PAGER: use only the bare first word. | |
| pager=${pager#"${pager%%[![:space:]]*}"} # trim leading whitespace | |
| pager=${pager%%[[:space:]]*} # keep first word | |
| [ -n "$pager" ] || pager="less" | |
| ;; | |
| esac | |
| # check for PAGER dependencies | |
| case "$pager" in | |
| # POSIX command -v has no portable -- guard; reject option-like command names. | |
| -* ) pager="cat" ;; | |
| esac | |
| if ! command -v "$pager" >/dev/null; then | |
| pager="cat" | |
| if ! command -v "$pager" >/dev/null; then | |
| die "PAGER cannot be determined; and cat fallback not found in PATH. Aborting." | |
| fi | |
| fi | |
| ###################################################################### | |
| # START getopts related code; POSIX-tested long and short option support | |
| # Thank you: https://stackoverflow.com/users/519360/adam-katz | |
| # https://stackoverflow.com/a/28466267/490487 | |
| # -: enables getopts to recognise and parse long options (like --help) | |
| #+ by treating -- as a special option that captures the long option name in $OPTARG. | |
| #+ For long option support -: must be appended once to the getopts argument list | |
| while getopts h-: OPT; do | |
| # support long options: https://stackoverflow.com/a/28466267/519360 | |
| if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG | |
| OPT="${OPTARG%%=*}" # extract long option name | |
| OPTARG="${OPTARG#"$OPT"}" # extract long option argument (may be empty) | |
| OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=` | |
| fi | |
| case "$OPT" in | |
| h | help ) usage ;; | |
| \? ) usage_with_prompt ;; # error reported via getopts | |
| * ) printf 'Illegal option --%s\n' "$OPT" >&2 ; usage_with_prompt ;; # bad long option | |
| esac | |
| done | |
| shift $((OPTIND-1)) # remove parsed options and args from $@ list | |
| # END getopts | |
| # Handle positional argument (the file path) | |
| [ "$#" -eq 1 ] || die "Error: exactly one input file path required. See -h for usage." | |
| input_file="${1:?Error: input file path required. See -h for usage.}" | |
| # Validate the file exists and is not empty | |
| [ -f "$input_file" ] || { die "Error: file '$input_file' not found"; } | |
| [ -s "$input_file" ] || { die "Error: input file is empty"; } | |
| # pause reads from /dev/tty even though the loop reads paths from "$input_file". | |
| [ -r /dev/tty ] || die "Error: no readable controlling terminal found; cannot pause for user input" | |
| command -v cygstart >/dev/null || die "Error: cygstart not found in PATH" | |
| while IFS= read -r line || [ -n "$line" ]; do | |
| line="${line%"$CR"}" # strip carriage returns | |
| [ -z "$line" ] && continue # skip empty lines | |
| if [ ! -e "$line" ]; then | |
| printf 'Warning: file not found, skipping: %s\n' "$line" >&2 | |
| continue | |
| fi | |
| printf 'file: %s\n' "$line" | |
| pause "Press ENTER to cygstart the file, or CTRL+C to abort" | |
| cygstart "$line" || { | |
| printf 'Warning: cygstart failed, skipping: %s\n' "$line" >&2 | |
| continue | |
| } | |
| done < "$input_file" | |
| # 💡 The $input_file is expected to contain file paths one per line. | |
| #+ If some file paths contain a line break the script logic needs to be updated to handle such a scenario. | |
| # 👆 breakdown on the above loop | |
| # 1. read the $input_file txt file line by line - expects file paths one per line | |
| # 2. handle each line as opaque data | |
| # the while loop processes records line-by-line; IFS= prevents field splitting | |
| # and read -r prevents backslash interpretation. "$line" is passed directly as | |
| # one quoted argument to printf and cygstart, so filenames with spaces, special | |
| # chars, and command-like syntax ($(...), etc.) are safe from command injection. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment