Skip to content

Instantly share code, notes, and snippets.

@genedelisa
Created July 18, 2025 17:45
Show Gist options
  • Save genedelisa/407d3d3783d0092f43226e9af7aa5e10 to your computer and use it in GitHub Desktop.
Save genedelisa/407d3d3783d0092f43226e9af7aa5e10 to your computer and use it in GitHub Desktop.
#!/usr/bin/env zsh
# -*- mode: sh; sh-shell: zsh; sh-indentation: 4; sh-basic-offset: 4; coding: utf-8; -*-
# vim: ft=zsh:sw=4:ts=4:et
#
# Time-stamp: "Last Modified 2025-07-18 13:43:00 by Gene De Lisa, genedelisa"
#
# File: mkcatalog
#
# Generate a list of filenames on External drives
#
# Gene De Lisa
# [email protected]
# http://rockhoppertech.com/blog/
# License - http://unlicense.org
################################################################################
# https://google.github.io/styleguide/shellguide.html#s7-naming-conventions
# https://zsh.sourceforge.io/Doc/Release/Functions.html
# https://zsh.sourceforge.io/Doc/Release/Options.html
################################################################################
# _ _ _
# _ __ ___ | | _____ __ _| |_ __ _| | ___ __ _
# | '_ ` _ \| |/ / __/ _` | __/ _` | |/ _ \ / _` |
# | | | | | | < (_| (_| | || (_| | | (_) | (_| |
# |_| |_| |_|_|\_\___\__,_|\__\__,_|_|\___/ \__, |
# |___/
#
################################################################################
# -R reset zsh options
# -L options LOCAL_OPTIONS, LOCAL_PATTERNS and LOCAL_TRAPS will be set
emulate -LR zsh
# Enable nullglob to avoid errors if no files match
setopt nullglob
readonly local SCRIPT_NAME=${ZSH_SCRIPT:t:r}
readonly local logo=$(print -- $SCRIPT_NAME | figlet -c -f basic)
typeset -r version=0.1.0
################################################################################
autoload -Uz zsh-color-messages && zsh-color-messages
local loglevel=error
local use_files
local use_directories
local do_sort
local volume
local maxdepth=1
local destdir=~/External\ Drive\ Catalogs
local now=$(date +'%Y-%m-%d-%H-%M-%S')
local usage_text="""
Specify directories and/or files
${SCRIPT_NAME} [-d | --directories] [-f | --files] [-s | --sort] [--volume vol] [--maxdepth n]
-h --help
-v --verbose
-d --directories
-f --files
-s --sort
--volume vol
--maxdepth n
--destdir path
${SCRIPT_NAME} -d -s
${SCRIPT_NAME} -f -s
${SCRIPT_NAME} -d -f -s
${SCRIPT_NAME} -d -f -s -m 2
${SCRIPT_NAME} -dfsm 2
${SCRIPT_NAME} --directories --files --sort --maxdepth 2
${SCRIPT_NAME} -d -f -s --volume /Volumes/WDElements5TB
${SCRIPT_NAME} --directories --files --sort --maxdepth 2 --destdir $HOME/catalogs
${SCRIPT_NAME} -d -f -s --maxdepth 2 --volume /Volumes/WDElements5TB
${SCRIPT_NAME} -dfsm 2 --volume /Volumes/WDElements5TB
"""
###############################################################################
parse_commandline() {
local -A local_options
zparseopts -a local_options -D -E -F -- \
h=help_opt -help=help_opt \
v=verbose_opt -verbose=verbose_opt \
-volume:=volume_opt \
-destdir:=destdir_opt \
m:=maxdepth_opt -maxdepth:=maxdepth_opt \
f=files_opt -files=files_opt \
d=directories_opt -directories=directories_opt \
s=sort_opt -sort=sort_opt \
|| {
>&2 print -- "zparseopts error" ${status}
>&2 print -- "zparseopts error" ${local_options}
return 1
}
# the -D flag removed the options
# now it's an array
non_flag_args="$@"
nonflag_args_array=("$@")
[[ -n ${help_opt} ]] && {
clear
print -rC1 -- $logo
print -rC1 -- $usage_text
exit 0
}
[[ -n ${verbose_opt} ]] && {
ZSH_DEBUG_PRINT=t
loglevel=info
}
[[ -n ${files_opt} ]] && {
use_files=t
zsh_debug_message "use_files $use_files"
}
[[ -n ${directories_opt} ]] && {
use_directories=t
zsh_debug_message "use_directories $use_directories"
}
[[ -n ${sort_opt} ]] && {
sort=t
zsh_debug_message "sort"
}
[[ ${#volume_opt} -ge 2 ]] && {
volume=${${volume_opt:+"${volume_opt[2]/#=/}"}}
zsh_debug_message "volume $volume"
}
[[ ${#maxdepth_opt} -ge 2 ]] && {
maxdepth=${${maxdepth_opt:+"${maxdepth_opt[2]/#=/}"}}
zsh_debug_message "maxdepth $maxdepth"
}
[[ ${#destdir_opt} -ge 2 ]] && {
destdir=${${destdir_opt:+"${destdir_opt[2]/#=/}"}}
zsh_debug_message "destdir $destdir"
}
return 0
}
###############################################################################
choose_volume() {
print -- "choosing volume"
local -a volumes=($(print -- /Volumes/*(N/) 2>/dev/null ))
local -a selected_files=()
while IFS= read -r -d '' file
do
selected_files+=("$file")
done < <(printf '%s\0' "${volumes[@]}" | \
fzf --read0 --print0 \
--bind "esc:abort" \
--bind 'ctrl-v:toggle-preview' \
--bind 'ctrl-y:preview-up' \
--bind 'ctrl-e:preview-down' \
--bind 'ctrl-b:preview-page-up' \
--bind 'ctrl-f:preview-page-down' \
--bind 'ctrl-j:preview-half-page-up' \
--bind 'ctrl-k:preview-half-page-down' \
--bind 'shift-up:preview-top' \
--bind 'shift-down:preview-bottom' \
--bind 'alt-up:half-page-up' \
--bind 'alt-down:half-page-down' \
--header='ctrl-a: select all ctrl-d: deselect all \
enter: open selected escape: abort' \
--ansi \
--no-sort \
--margin='1,2,1,2' \
--preview 'fzf-gd-preview {}'
)
volume=$selected_files
zsh_debug_message "selected volume $volume"
zsh_debug_message "use_directories $use_directories"
return 0
}
###############################################################################
process() {
# the pruning is to not print
# /Volumes/WDElements5TB/.Spotlight-V100
# /Volumes/WDElements5TB/.fseventsd
[[ -n ${use_directories} ]] && {
print -- just directories
local destfile=$destdir/${volume:t}.dirs.$maxdepth.$now.txt
zsh_debug_message "Writing to $destfile"
print -n -- $destfile | pbcopy
command date --iso-8601=seconds > $destfile
command df -h $volume >> $destfile
[[ -n ${sort} ]] && {
command find $volume -maxdepth $maxdepth -type d \
\( -path '**/.Spotlight-V100' -o -path '**/.fseventsd' \) -prune -o -print 2>/dev/null \
| LC_ALL=C sort --ignore-case >> $destfile
} || {
command find $volume -maxdepth $maxdepth -type d \
\( -path '**/.Spotlight-V100' -o -path '**/.fseventsd' \) -prune -o -print 2>/dev/null >> $destfile
}
}
[[ -n ${use_files} ]] && {
zsh_debug_message "all files"
local destfile=$destdir/${volume:t}.files.$maxdepth.$now.txt
zsh_debug_message "Writing to $destfile"
print -n -- $destfile | pbcopy
command date --iso-8601=seconds > $destfile
command df -h $volume >> $destfile
[[ -n ${sort} ]] && {
command find $volume -maxdepth $maxdepth \
\( -path '**/.Spotlight-V100' -o -path '**/.fseventsd' \) -prune -o -print 2>/dev/null \
| LC_ALL=C sort --ignore-case >> $destfile
} || {
command find $volume -maxdepth $maxdepth \
\( -path '**/.Spotlight-V100' -o -path '**/.fseventsd' \) -prune -o -print 2>/dev/null >> $destfile
}
}
return 0
}
################ main flow
parse_commandline "${@}"
[[ ${status} -ne 0 ]] && {
>&2 print -- "Oops. zparseopts error" ${status}
exit $status
}
[[ -z ${use_directories} && -z ${use_files} ]] && {
>&2 print -- $usage_text
exit 2
}
[[ -n ${volume} ]] && {
[[ ${loglevel} == "info" ]] && zsh_debug_message "volume is set from flag: ${volume}"
}
[[ -z ${volume} ]] && {
choose_volume
}
[[ ${loglevel} == "info" ]] && {
zsh_debug_message "selected volume $volume"
zsh_debug_message "use_directories $use_directories"
zsh_debug_message "use_files $use_files"
zsh_debug_message "sort $sort"
zsh_debug_message "maxdepth $maxdepth"
zsh_debug_message "destdir $destdir"
}
process
exit $status
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment