Skip to content

Instantly share code, notes, and snippets.

@kyle0r
Last active May 23, 2026 23:41
Show Gist options
  • Select an option

  • Save kyle0r/53b322cc558e60ba121721ee57093e5c to your computer and use it in GitHub Desktop.

Select an option

Save kyle0r/53b322cc558e60ba121721ee57093e5c to your computer and use it in GitHub Desktop.
cygwin: open arbitrary file paths with pause
*.sh text eol=lf
#!/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