Files
dotfiles/.config/zsh/zshrc.d/40-functions.zsh
Julian Prein 6672387bee zsh:funcs:finddup: Use byte size for first filter
Use the byte size as first filter instead of the size in 1KB blocks.

This way the filter is way more accurate and filters out more files
for which the md5sum does not need to be calculated.
2022-12-28 02:02:04 +01:00

622 lines
15 KiB
Bash

## Author: druckdev
## Created: 2019-08-28
# Compares two pdfs based on the result of pdftotext
pdfdiff() {
if [[ $# -eq 2 && -r "$1" && -r "$2" ]]; then
diff <(pdftotext "$1" -) <(pdftotext "$2" -)
else
echo "something went wrong" 2>&1
return 1
fi
}
# Gets Passwd from bitwarden and copies it into the clipboard
bwpwd() {
if bw "get" "password" "$@" >/dev/null; then
bw "get" "password" "$@" | tr -d '\n' | setclip
else
bw "get" "password" "$@"
fi
}
# mkdir wrapper that changes into the created directory if only one was given
mkcd () {
# Create directory
command mkdir "$@" || return
# Remove flags and their arguments
nargs="$#"
for ((i = 0; i < nargs; i++)); do
if [[ ${1[1]} != '-' || $1 = '-' ]]; then
# Skip all non-flags
set -- "${@:2}" "$1"
else
# When `-m` is given, shift it's MODE argument as well
! [[ $1 =~ ^-([^-].*)?m$ ]] || { shift; i+=1 }
# Stop the loop on `--`
[[ $1 != '--' ]] || { shift; break }
shift
fi
done
# cd into the created directory if only one was specified
[[ $# -eq 1 && -d $1 ]] || return 0
# append a slash to change into the new directory instead of back to the
# last visited one
[[ $1 != '-' ]] || 1='-/'
cd "$1"
pwd
}
# Encode and decode qr-codes
qr() {
if [[ $# -eq 1 && -r "$1" ]]; then
zbarimg "$1"
else
qrencode "$@"
fi
}
# Edit config file
conf() {
local CONF_EDITOR
if [[ -n "$EDITOR" ]]; then
CONF_EDITOR="$EDITOR"
elif (( $+commands[vim] )); then
CONF_EDITOR=vim
elif (( $+commands[nano] )); then
CONF_EDITOR=nano
else
CONF_EDITOR=cat
fi
# Parse otions
while getopts "e:" opt 2>/dev/null; do
case $opt in
e) CONF_EDITOR="$OPTARG";;
*) printf "\033[1;31mUsage: $0 [-e <editor>] <program>[/subdirs] [<config_file>]\n\033[0m" >&2
return 1 ;;
esac
done
shift $(( $OPTIND - 1 ))
# conf needs an argument
if [[ $# -eq 0 ]]; then
printf "\033[1;31mPlease specify a config.\n\033[0m" >&2
return 1
fi
# search for program name in XDG_CONFIG_HOME and $HOME
local CONF_DIR="$(_get_config_dir "$1")"
if (( $? )); then
printf "\033[1;31mFalling back to $HOME.\n\033[0m" >&2
CONF_DIR="$HOME"
fi
# If specific name is given, open file.
if [[ $# -gt 1 ]]; then
"$CONF_EDITOR" "$CONF_DIR/$2"
return
fi
# possible config-file names + same but hidden
local -a CONF_PATTERNS
CONF_PATTERNS=(
"$1.conf"
"$1.config"
"${1}rc"
"config"
"conf"
"$1.yml"
"$1.yaml"
"config.ini"
"$1"
)
# check if config file exists
for config in $CONF_PATTERNS; do
if [[ -e "$CONF_DIR/$config" ]]; then
$CONF_EDITOR "$CONF_DIR/$config"
return 0
elif [[ -e "$CONF_DIR/.$config" ]]; then
$CONF_EDITOR "$CONF_DIR/.$config"
return 0
fi
done
# if no config was found in a location other than HOME, look again in HOME.
# (For cases like default vim with ~/.vim/ and ~/.vimrc)
if [[ "$CONF_DIR" != "$HOME" ]];then
for config in $CONF_PATTERNS; do
# Only look for hidden files
if [[ -e "$HOME/.$config" ]]; then
$CONF_EDITOR "$HOME/.$config"
return 0
fi
done
fi
printf "\033[1;31mCould not find config file.\n\033[0m" >&2
return 1
}
# Change into config dir
c() {
CONF_DIR="$(_get_config_dir $*)"
if [[ $? -eq 0 ]]; then
cd "$CONF_DIR"
else
printf "$CONF_DIR" >&2
return 1
fi
}
# Get config directory
_get_config_dir() {
if [[ $# -gt 1 ]]; then
printf "\033[1;31mPlease specify one config.\n\033[0m" >&2
return 1
elif [[ $# -eq 0 ]]; then
echo "${XDG_CONFIG_HOME:-$HOME/.config}"
elif [[ -d "${XDG_CONFIG_HOME:-$HOME/.config}/$1" ]]; then
echo "${XDG_CONFIG_HOME:-$HOME/.config}/$1"
elif [[ -d "$HOME/.$1" ]]; then
echo "$HOME/.$1"
else
printf "\033[1;31mCould not find config home.\n\033[0m" >&2
return 1
fi
}
# Function that resolves a command to the end
resolve() {
local verbose cmd old_cmd args last_line
local -a full_cmd
# Since there are multiple occasions were the same line would be printed
# multiple times, this function makes the output in combination with the
# verbose flag cleaner.
local uniq_print() {
if [[ "$*" != "$last_line" ]]; then
printf "%s\n" "$*"
last_line="$*"
fi
}
while getopts v flag 2>/dev/null; do
[[ "$flag" != "v" ]] || verbose=1
done
shift $((OPTIND - 1))
full_cmd=("$@")
cmd="${full_cmd[1]}"
(( ! verbose )) || uniq_print "${full_cmd[@]}"
# Resolve aliases
while [[ "$cmd" != "$old_cmd" ]]; do
out="$(command -v "$cmd")"
# NOTE: cannot be combined for the case that $cmd is not an alias
out="${out#alias $cmd=}" # Extract the alias or command
out="${${out#\'}%\'}"
# Beware of potential empty element leading to print trailing whitespace
if (( $#full_cmd > 1)); then
full_cmd=($out "${full_cmd[2,-1]}")
else
full_cmd=($out)
fi
(( ! verbose )) || uniq_print "${full_cmd[@]}"
old_cmd="$cmd"
cmd="${full_cmd[1]}"
done
# Resolve symlinks
if [[ -e "$cmd" ]]; then
# When we are not verbose a call to realpath is sufficient and way
# faster.
if (( ! verbose )); then
cmd="$(realpath "$cmd")"
full_cmd=("$cmd" "${full_cmd[2,-1]}")
else
old_cmd=
while [[ "$cmd" != "$old_cmd" ]]; do
# Get filename with potential symlink target
out="$(stat -c "%N" "$cmd")"
# NOTE: cannot be combined for the case that $cmd is not symlink
out="${out#*-> }"
out="${${out#\'}%\'}"
# Beware of symlinks pointing to a relative path
[[ "${out[1]}" = '/' ]] || out="$(dirname "$cmd")/$out"
# Beware of potential empty element leading to print trailing
# whitespace
if (( $#full_cmd > 1)); then
full_cmd=("$out" "${full_cmd[2,-1]}")
else
full_cmd=("$out")
fi
uniq_print "${full_cmd[@]}" # verbose is set
old_cmd="$cmd"
cmd="${full_cmd[1]}"
done
fi
fi
uniq_print "${full_cmd[@]}"
}
# Grep a keyword at the beginning of a line (ignoring whitespace) in a man page
mangrep() {
if [[ $# -lt 2 ]]; then
printf "usage: mangrep <man page> <pattern> [<man flags>]\n" >&2
printf "example: mangrep bash \"(declare|typeset)\"\n" >&2
return 1
fi
local page="$1" pattern="$2"
shift 2
man -P "less -p \"^ *${pattern}\"" "$@" "${file}"
}
safe-remove() {
[[ $# -gt 0 ]] || return 1
[[ -e "$1" ]] || return 1
sync
if mount | grep -q "$1" && ! udisksctl unmount -b "$1"; then
lsof "$1"
return 1
fi
udisksctl power-off -b "/dev/${$(lsblk -no pkname "$1"):-${1#/dev/}}"
}
crypt-open() {
emulate -L zsh -o err_return -o pipe_fail
[[ $# -gt 0 ]]
[[ -b "$1" ]]
local name mount_point
name=crypt_"${1:t}"
sudo cryptsetup open "$1" "$name"
udisksctl mount -b /dev/mapper/"$name"
mount_point="$(
findmnt -lo SOURCE,TARGET \
| grep -F /dev/mapper/"$name" \
| awk '{ print $2; }'
)"
[[ -d $mount_point ]] && cd "$mount_point"
# create link in $HOME/mounts
[[ ! -e "$HOME/mounts/${mount_point:t}" ]] \
|| { echo "~/mounts/${mount_point:t} exists" >&2; return 1; }
mkdir -p ~/mounts/
ln -s "$mount_point" ~/mounts/
}
crypt-close() {
emulate -L zsh -o err_return -o pipe_fail
[[ $# -gt 0 ]]
[[ -b "$1" ]]
sync
local name mount_point
name=crypt_"${1:t}"
mount_point="$(
findmnt -lo SOURCE,TARGET \
| grep -F /dev/mapper/"$name" \
| awk '{ print $2; }'
)"
if
mount | grep -q /dev/mapper/"$name" \
&& ! udisksctl unmount -b /dev/mapper/"$name"
then
lsof /dev/mapper/"$name"
return 1
fi
if ! sudo cryptsetup close "$name"; then
sudo cryptsetup status "$name"
return 1
fi
udisksctl power-off -b "$1"
rm ~/mounts/"${mount_point:t}" \
&& rmdir --ignore-fail-on-non-empty ~/mounts/ \
|| echo "~/mounts/${mount_point:t} did not exist"
}
if (( $+commands[trash] )); then
# List items in trash if no argument is specified
trash() {
if (( ! $# )); then
command trash-list
else
command trash "$@"
fi
}
fi
# Move one or more file(s) but keep a symlink to the new location.
mvln() {
if (( # < 2 )); then
printf "$0: missing file operand\n"
return 1
elif (( # == 2 )); then
# When used for renaming only the dirname has to exist
if [[ ! -d $(dirname "$2") ]]; then
printf "$0: cannot move '$1' to '$2': No such file or directory\n"
return 1
fi
elif [[ ! -d ${@[-1]} ]]; then
printf "$0: target '${@[-1]}' is not a directory\n"
return 1
fi
reg=0
for file in "${@[1,-2]}"; do
# If the target is a directory, `file` will end up in it
# NOTE: We need absolute paths here for executions like `$0 foo/bar .`
# TODO: When do we want/can we use relative links? Only when file is in
# current dir?
if [[ -d ${@[-1]} ]]; then
target="${@[-1]:A}/$(basename "$file")"
else
target="${@[-1]:A}"
fi
if ! command mv -i "$file" "${@[-1]}"; then
reg=1
continue
fi
# NOTE: `ln` does not like trailing slashes on the last argument
ln -s "$target" "${file%/}"
done
return $reg
}
# cd-wrapper that recognizes a trailing `ls` behind the path (When not properly
# pressing <CR>)
cd() {
# Call `ls` on paths that end on '/ls' and don't exist with the suffix.
# (When not properly pressing <CR>)
if [[ ! -e ${@[-1]} && -e ${@[-1]%%/ls} ]]; then
builtin cd "${@[1,-2]}" "${@[-1]%%ls}"
pwd
ls
else
builtin cd "$@"
fi
}
# This is meant for adding a quick fix to a commit.
# It automatically rebases a given commit (defaults to HEAD), applies the given
# stash (defaults to last) and finishes the rebase.
git-rebase-add-stash() {
: ${1:=HEAD~}
[[ "$(git cat-file -t "$1")" = "commit" ]] || return 1
(( $(git stash list | wc -l) )) || { echo "No stashes" >&2; return 1; }
# Substitute the first 'pick' with 'edit' in the rebase todo, apply the
# stash & finish
EDITOR='sed -i "1s/^pick/edit/"' \
git rebase -i "$1" &&
git stash apply "${2:-0}" &&
git add -u &&
git rebase --continue
}
# Display the log for the staged files (excluding additions, as they do not have
# a history and I prefer the full log instead of nothing in that case).
git-log-staged-files() {
# No quotes around the filename expansion as they are only field splitted on
# newlines anyway and `git log -- ""` complains with:
#
# fatal: empty string is not a valid pathspec.
#
# As the `git-diff` command can return nothing, this is important.
#
# NOTE: Use `log.follow` instead of `--follow` to support multiple arguments
git -c log.follow log --name-only "$@" -- \
${(f)"$(git diff --name-only --cached --diff-filter=a)"}
}
# Create copy with a .bkp extension
bkp() {
for f; do
command cp -i "$f"{,.bkp}
done
}
# Reverse bkp()
unbkp() {
for f; do
if [[ ${f%.bkp} != $f ]]; then
command mv -i "$f" "${f%.bkp}"
elif [[ -e $f.bkp ]]; then
command mv -i "$f.bkp" "$f"
fi
done
}
# Create a virtual environment for python including a .envrc that loads the venv
create_venv() {
[[ ! -e venv ]] || return 0
python -m venv venv
if (( $+commands[direnv] )); then
ln -s ~/.local/share/direnv/templates/python-venv.envrc .envrc
direnv allow
fi
}
# Open READMEs in a pager when going into a directory that contains one.
# See 45-hooks.zsh
_page_readme_chpwd_handler() {
local readme
local -a readmes=(README.md README.txt README Readme.md Readme.txt Readme
readme.md readme.txt readme)
for readme in "$readmes[@]"; do
[[ -e "$readme" ]] || continue
${PAGER:-less} "$readme"
break
done
}
# I sometimes find `pgrep` not matching the processes I am searching for, but
# `ps aux | grep ...` did not disappoint yet.
psgrep() {
# - Set EXTENDED_GLOB for the `b` globbing flag.
emulate -L zsh -o extendedglob
# print column info
ps u | head -1
for arg; do
# Pass to grep directly if it looks like a regex
if [[ "$arg" =~ '[][$|.*?+\\()^]' ]]; then
ps aux | grep -E "$arg"
continue
fi
# Substitute the captured first character with itself surrounded by
# brackets. The `(#b)` turns on backreferences, storing the match in the
# array $match (in this case with only one element).
# So for example: "pattern" -> "[p]attern"
# This has the effect that the `grep` does not grep itself in the processes
# list.
ps aux | grep "${arg/(#b)(?)/[$match]}"
done
}
# Use shellcheck.net if shellcheck is not installed.
shellcheck() {
if (( $+commands[shellcheck] )); then
command shellcheck "$@"
return
fi
printf >&2 \
"Using www.shellcheck.net. You might want to install shellcheck.\n\n"
local url json_parser arg
url='https://www.shellcheck.net/shellcheck.php'
json_parser="${${commands[jq]:-cat}:c}"
for arg; do
if [[ ! -r $arg ]]; then
printf "%s\n" "$arg: File does not exist or is non-readable" >&2
continue
fi
curl -sS "$url" -X POST --data-urlencode script@"$arg" \
| "$json_parser"
done
}
# Find files that end with one of multiple given suffixes.
#
# Usage:
# suffix sfx... [-- path...]
#
# `sfx` is given to `find` in the form `-name "*$sfx"`.
# `path` is given as starting point to `find`, defaulting to `.`.
suffix() {
if (( ! $+commands[find] )); then
printf >&2 "find not installed\n"
return 1
fi
local i=1
for arg; do
[[ $arg != "--" ]] || break
: "$((i++))"
done
# NOTE: if "--" is not included in $@, i will be greater than $#, and no
# starting point is passed to `find`, which then defaults to `.`.
local -a names
# Take everything before "--" and quote special characters
names=( "${(@q)@:1:$((i-1))}" )
# Prepend an `*` to every element
names=( "${(@)names//#/*}" )
# Join with " -o -name " and then split again using shell parsing
names=( "${(@zj: -o -name :)names}" )
# Pass starting points and the name arguments
find "${@:$((i+1))}" -name "${(@)names}"
}
# Find duplicate files
finddup() {
# find all files, filter the ones out with unique size, calculate md5 and
# print duplicates
# TODO: Fix duplicate lines output in the awk script that currently `sort
# -u` handles
# TODO: Use cksum to calculate faster CRC with custom awk solution to print
# duplicates, as `uniq -w32` breaks through the different CRC lengths.
find "$@" -type f -exec du -b '{}' '+' \
| sort \
| awk '{ if (!_[$1]) { _[$1] = $0 } else { print _[$1]; print $0; } }' \
| sort -u \
| cut -d$'\t' -f2- \
| xargs -d'\n' md5sum \
| sort \
| uniq -w32 --all-repeated=separate \
| cut -d' ' -f3-
}
# Wrapper around tmsu that searches for .tmsu/db in all parent directories and
# fallbacks to XDG_DATA_HOME if not found.
tag() {
if (( ! $+commands[tmsu] )); then
printf >&2 "tmsu not installed.\n"
return 1
fi
local db dir="$PWD" std=".tmsu/db"
# Go up directories until root to find .tmsu/db
until [[ -e $dir/$std || $dir = / ]]; do
dir="${dir:h}"
done
db="$dir/$std"
# Fallback to XDG_DATA if .tmsu/db was not found in one of the parent dirs.
if [[ ! -e $db ]]; then
db="${XDG_DATA_HOME:-$HOME/.local/share}"/tmsu/db
mkdir -p "${db:h}"
fi
env TMSU_DB="$db" tmsu "$@"
}
# Display the help for a given python module/function/etc. Try to guess when
# something needs an import.
pyhelp() {
local py_exec import_statement
if (( $+commands[python] )); then
py_exec=python
elif (( $+commands[python3] )); then
py_exec=python3
elif (( $+commands[python2] )); then
py_exec=python2
else
print >&2 "Python not installed."
return 1
fi
for arg; do
import_statement=
if [[ $arg =~ '^([^.]*)\.' ]]; then
import_statement="import $match[1]; "
fi
$py_exec -c "${import_statement}help($arg)"
done
}