├── bash-timer ├── .gitignore ├── .travis.yml ├── README.md ├── install ├── bash-timer.sh └── assets │ └── bash-preexec.sh ├── my-ip.sh ├── american-date ├── tests ├── git-filter-copy │ └── test.sh └── test-tar-stats.sh ├── sudoers.d └── 00_prompt_once ├── esoteric ├── git-shallow-pull ├── arch-pacman-dupe-cleaner ├── arch-upgrade-postgres ├── clone-github-repos.php └── init-btrfs-rootfs ├── framework ├── is_root └── wait_until_mouse_or_keyboard_event ├── bash_profile ├── launch-browser ├── wifi-show-password ├── bashrc ├── watermark.sh ├── cron.hourly ├── php-clean-tmp ├── bettergist-backup └── btrfs-snapshot ├── sync-watch ├── random-file ├── ls-by-min ├── cron.daily ├── 01_purge-locales ├── btrfs-snapshot └── 00_clear-cache ├── image-mp3-to-video ├── ssh-autologin ├── ssh-keyphrase-only-once.installer ├── stream-to-youtube ├── x265.sh ├── git-same-sig-time ├── tar-stats-src ├── help.sh ├── extract.sh ├── create.sh ├── build.sh └── test.sh ├── changelog-maker-lite ├── tar-sorted ├── gitconfig ├── git-commit-at-modded-time ├── git-mtime ├── turn-off-monitors ├── bash_rc.aliases ├── wifi-autorun-on-connect.installer ├── git-filter-copy ├── git-change-author ├── git-shift-time ├── obs-global-hotkeys ├── CHANGELOG.md ├── README.cn.md └── LICENSE.cc-by.md /bash-timer/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /my-ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl -s https://api.ipify.org 4 | echo "" 5 | -------------------------------------------------------------------------------- /american-date: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | date +"%a, %e %B %Y %H:%M:%S %Z" 4 | 5 | -------------------------------------------------------------------------------- /bash-timer/.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | script: 4 | - curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.0/install | bash 5 | -------------------------------------------------------------------------------- /tests/git-filter-copy/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf dockerize 4 | ../../git-filter-copy /code/dockerize . 5 | diff -r dockerize dockerize-verify 6 | -------------------------------------------------------------------------------- /sudoers.d/00_prompt_once: -------------------------------------------------------------------------------- 1 | ## Only ask for the password once for all TTYs per reboot. 2 | ## See https://askubuntu.com/a/643146 3 | Defaults !tty_tickets 4 | Defaults timestamp_timeout = -1 5 | 6 | -------------------------------------------------------------------------------- /esoteric/git-shallow-pull: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # @see https://stackoverflow.com/a/41081908/430062 4 | BRANCH=git branch | awk '{print $2}' 5 | 6 | git fetch --depth=1 7 | git reset --hard "origin/${BRANCH}" 8 | git clean -dfx 9 | 10 | -------------------------------------------------------------------------------- /framework/is_root: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function is_root() 4 | { 5 | [ "$EUID" -eq 0 ]; 6 | } 7 | 8 | 9 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 10 | if is_root; then 11 | echo "Running as root" 12 | else 13 | echo "Not running as root" 14 | fi 15 | fi -------------------------------------------------------------------------------- /bash_profile: -------------------------------------------------------------------------------- 1 | # 2 | # ~/.bash_profile 3 | # 4 | 5 | [[ -f ~/.bashrc ]] && . ~/.bashrc 6 | 7 | # Run ssh-agent ONCE per user per system boot. 8 | # [installed via https://github.com/hopeseekr/BashScripts] 9 | # @see https://unix.stackexchange.com/a/217223/15780 10 | if [ ! -S ~/.ssh/ssh_auth_sock ]; then 11 | eval `ssh-agent` 12 | ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock 13 | fi 14 | export SSH_AUTH_SOCK=~/.ssh/ssh_auth_ 15 | 16 | alias kubectl='minikube kubectl --' 17 | # Make gauth accept a case insensitive search argument. 18 | function gauth() { `which gauth` | grep -i ^${1}; } 19 | -------------------------------------------------------------------------------- /launch-browser: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$1" ]; then 4 | echo "Error: You must specify the browser command." 5 | exit 1 6 | fi 7 | 8 | CMD="/usr/bin/$1" 9 | if [ ! -f "$CMD" ]; then 10 | if [ -f "$1" ]; then 11 | CMD="$1" 12 | else 13 | echo "Error: Cannot find /usr/bin/$1" 14 | exit 2 15 | fi 16 | fi 17 | 18 | shift 1 19 | 20 | # Launch in Wayland, if detected. 21 | USE_WAYLAND="" 22 | if [ "$XDG_SESSION_TYPE" == "wayland" ]; then 23 | USE_WAYLAND="--enable-features=UseOzonePlatform --ozone-platform=wayland" 24 | fi 25 | 26 | "$CMD" --password-store=gnome-libsecret "${USE_WAYLAND}" "$@" 27 | -------------------------------------------------------------------------------- /wifi-show-password: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Show Active WIFI Password # 4 | # # 5 | # Shows the Wi-Fi password for the currently connected AP. # 6 | # # 7 | # Part of HopeSeekr's BashScripts Collection # 8 | # https://github.com/hopeseekr/BashScripts/ # 9 | # # 10 | # Copyright © 2025 Theodore R. Smith # 11 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 12 | # # 13 | # License: Creative Commons Attribution v4.0 International # 14 | ######################################################################### 15 | 16 | nmcli dev wifi show-password 17 | -------------------------------------------------------------------------------- /bashrc: -------------------------------------------------------------------------------- 1 | # A better $PS1 2 | export PS1="\[\033[38;5;12m\][\[$(tput sgr0)\]\[\033[38;5;10m\]hopeseekr\[$(tput sgr0)\]\[\033[38;5;12m\]@\[$(tput sgr0)\]\[\033[38;5;7m\]\h\[$(tput sgr0)\]\[\033[38;5;12m\]]\[$(tput sgr0)\]\[\033[38;5;15m\]: \[$(tput sgr0)\]\[\033[38;5;7m\]\w\[$(tput sgr0)\]\[\033[38;5;12m\]>\[$(tput sgr0)\]\[\033[38;5;10m\]\\$\[$(tput sgr0)\]\[\033[38;5;15m\] \[$(tput sgr0)\]" 3 | 4 | # A Developer's $PATH 5 | export PATH=./vendor/bin:./node_modules/.bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/opt/android-sdk/tools:/opt/android-sdk/tools/bin:$PATH 6 | 7 | # Bash Timer 8 | # See https://github.com/hopeseekr/bash-timer 9 | [[ -f ~/.bash-timer.sh ]] && source ~/.bash-timer.sh 10 | 11 | # Hide snaps and flatpak mounts. 12 | alias df='df -x squashfs | grep -v fuse'\ 13 | 14 | # Make `watch` honor ~/.bashrc aliases. 15 | alias watch='watch ' 16 | 17 | #source ~/.bash_rc.aliases 18 | 19 | # See https://github.com/rcaloras/bash-preexec 20 | # **WARNING:** This must be the last line of your .bashrc. 21 | # Source our file at the end of our bash profile (e.g. ~/.bashrc, ~/.profile, or ~/.bash_profile) 22 | [[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh 23 | 24 | -------------------------------------------------------------------------------- /framework/wait_until_mouse_or_keyboard_event: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function wait_until_mouse_or_keyboard_movement() 4 | { 5 | # Ensure libinput-tools is installed 6 | if ! command -v libinput >/dev/null 2>&1; then 7 | echo "Error: libinput-tools is not installed. Please install it and try again." 8 | exit 1 9 | fi 10 | 11 | if ! command -v sudo >/dev/null 2>&1; then 12 | echo "Error: sudo is not installed. Please install it and try again." 13 | exit 1 14 | fi 15 | 16 | # Turn off the monitors. 17 | 18 | # Start monitoring input events 19 | #echo "Monitoring input events. Press Ctrl+C to stop." 20 | sudo stdbuf -oL libinput debug-events | while read -r line; do 21 | case "$line" in 22 | *"KEYBOARD_KEY"*) 23 | echo "KEYBOARD_KEY"; return 0; 24 | ;; 25 | *"POINTER_BUTTON"*) 26 | echo "MOUSE_CLICK"; return 0; 27 | ;; 28 | *"POINTER_MOTION"*) 29 | echo "MOUSE_MOVED"; return 0; 30 | ;; 31 | esac 32 | done 33 | } 34 | 35 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 36 | wait_until_mouse_or_keyboard_movement 37 | fi 38 | -------------------------------------------------------------------------------- /watermark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Video Watermarker # 4 | # # 5 | # Easily embed your own image watermark onto videos (via ffmpeg). # 6 | # # 7 | # Part of HopeSeekr's BashScripts Collection # 8 | # https://github.com/hopeseekr/BashScripts/ # 9 | # # 10 | # Copyright © 2020 Theodore R. Smith # 11 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 12 | # # 13 | # License: Creative Commons Attribution v4.0 International # 14 | ######################################################################### 15 | 16 | # @FIXME: This *really* needs to be abstracted out. 17 | ffmpeg -i "$1" -i /code/SurviveCorona.how/src/assets/transparent_hopeseekr_twitter.png -filter_complex "overlay=10:10" -codec:a copy "$2" 18 | -------------------------------------------------------------------------------- /cron.hourly/php-clean-tmp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # PHP Temp file Cleaner # 4 | # # 5 | # Removes unnecessary PHP temp files. # 6 | # # 7 | # Part of HopeSeekr's BashScripts Collection # 8 | # https://github.com/hopeseekr/BashScripts/ # 9 | # # 10 | # Copyright © 2020 Theodore R. Smith # 11 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 12 | # # 13 | # License: Creative Commons Attribution v4.0 International # 14 | ######################################################################### 15 | 16 | cd /tmp 17 | 18 | # Remove PHP temporary files 19 | rm -f $(ls /tmp | grep -P '^[a-zA-Z0-9]{6}$') 20 | 21 | # Remove PHPUnit temp files 22 | rm -f /tmp/tmp.* 23 | 24 | # Remove phpstan temp files 25 | rm -rf /tmp/phpstan 26 | -------------------------------------------------------------------------------- /sync-watch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Dirty Data Sync Watcher # 4 | # # 5 | # Outputs a tiny, updating display of how many megabytes are queued # 6 | # in memory, waiting to be written to disk. # 7 | # # 8 | # This helps you visualize when it will be safe to unmount a disk # 9 | # or turn off your system. # 10 | # # 11 | # Part of HopeSeekr's BashScripts Collection # 12 | # https://github.com/hopeseekr/BashScripts/ # 13 | # # 14 | # Copyright © 2020 Theodore R. Smith # 15 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 16 | # # 17 | # License: Creative Commons Attribution v4.0 International # 18 | ######################################################################### 19 | 20 | /bin/watch -d grep -e Dirty: -e Writeback: /proc/meminfo 21 | 22 | -------------------------------------------------------------------------------- /random-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Random File Picker # 4 | # # 5 | # Randomly picks a file or directory in the given directory or $PWD # 6 | # and displays it. # 7 | # # 8 | # I use this to let Fate decide which movie I'm going to watch! # 9 | # # 10 | # Part of HopeSeekr's BashScripts Collection # 11 | # https://github.com/hopeseekr/BashScripts/ # 12 | # # 13 | # Copyright © 2020 Theodore R. Smith # 14 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 15 | # # 16 | # License: Creative Commons Attribution v4.0 International # 17 | ######################################################################### 18 | 19 | dir=${1-$PWD} 20 | 21 | # @see https://stackoverflow.com/a/18617295/430062 22 | files=($dir/*) 23 | printf "%s\n" "${files[RANDOM % ${#files[@]}]}" 24 | 25 | -------------------------------------------------------------------------------- /cron.hourly/bettergist-backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # PostgreSQL Auto Backupper (Hourly) # 4 | # # 5 | # Automatically makes compressed xz backups of a database every hour, # 6 | # but only if the DB's contents have changed. # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2020 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | ######################################################################### 16 | 17 | cd /code/bettergist-collector/database/backups 18 | sudo -u postgres pg_dump bettergist | xz -9 --threads=0 > bettergist-$(date +"%Y%m%d.%H").sql.xz 19 | 20 | # Delete dupes. https://unix.stackexchange.com/a/192712/15780 21 | md5sum bettergist*sql.xz | sort | awk 'BEGIN{lasthash = ""} $1 == lasthash {print $2} {lasthash = $1}' | xargs rm 22 | -------------------------------------------------------------------------------- /ls-by-min: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # LS By Min # 4 | # # 5 | # Sorts the output of `ls` by file size, descending. # 6 | # # 7 | # If you want to sort ascending, try `ls-by-min | sort -r` # 8 | # # 9 | # Part of HopeSeekr's BashScripts Collection # 10 | # https://github.com/hopeseekr/BashScripts/ # 11 | # # 12 | # Copyright © 2020 Theodore R. Smith # 13 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 14 | # # 15 | # License: Creative Commons Attribution v4.0 International # 16 | ######################################################################### 17 | 18 | if [ -z "$1" ]; then 19 | echo "Error: You must specify a minimum file size (in MB)." 20 | exit 1 21 | fi 22 | 23 | FILE_SIZE=$1 24 | 25 | if [ "$2" = "-r" ]; then 26 | MAXDEPTH=512 27 | else 28 | MAXDEPTH=1 29 | fi 30 | 31 | find . -maxdepth ${MAXDEPTH} -type f -size "+${FILE_SIZE}M" -exec du -sm {} \; | sort -rnk1 | sed 's/^[0-9]\+\t*//g' 32 | 33 | -------------------------------------------------------------------------------- /cron.daily/01_purge-locales: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ######################################################################### 4 | # BTRFS Auto Snapshotter (Hourly) # 5 | # # 6 | # Automatically purges every non-EN locale every day. # 7 | # # 8 | # NOTE: This has only tested on Arch Linux. It might cripple your # 9 | # distribution's package manager (missing files). # 10 | # # 11 | # Part of HopeSeekr's BashScripts Collection # 12 | # https://github.com/hopeseekr/BashScripts/ # 13 | # # 14 | # Copyright © 2020 Theodore R. Smith # 15 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 16 | # # 17 | # License: Creative Commons Attribution v4.0 International # 18 | ######################################################################### 19 | 20 | 21 | # @see https://serverfault.com/a/37836/56309 22 | if [[ $(/usr/bin/id -u) -ne 0 ]]; then 23 | echo "Error: You *MUST* run this as sudo or root!" 24 | echo "" 25 | exit 99 26 | fi 27 | 28 | # Removes every locale except en_US. 29 | LOCALES=$(ls -1 /usr/share/locale/ | grep -v ^en) 30 | 31 | cd /usr/share/locale 32 | for locale in $LOCALES; do 33 | rm -rvf $LOCALES 34 | done 35 | -------------------------------------------------------------------------------- /image-mp3-to-video: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################################################# 3 | # image-mp3-to-video: Combines an image with an mp3 to create a H264 video. # 4 | # # 5 | # Usage: # 6 | # image-mp3-to-video [image] [mp3] # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2025 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | ################################################################################# 16 | 17 | if [[ -z "$1" ]]; then 18 | echo "Missing the image file." 19 | exit 1 20 | fi 21 | 22 | if [[ -z "$2" ]]; then 23 | echo "Missing the audio file." 24 | exit 1 25 | fi 26 | 27 | AUDIO_IN="$2" 28 | OUTPUT="${AUDIO_IN%.mp3}.mp4" 29 | 30 | ffmpeg -loop 1 -framerate 2 -vaapi_device "${VAAPI_DEVICE:-/dev/dri/renderD128}" \ 31 | -i "$1" -i "$2" \ 32 | -vf 'format=nv12,hwupload' \ 33 | -c:v h264_vaapi -b:v 2M \ 34 | -c:a aac -b:a 192k \ 35 | -pix_fmt nv12 -shortest \ 36 | -movflags +faststart "$OUTPUT" 37 | 38 | -------------------------------------------------------------------------------- /cron.daily/btrfs-snapshot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # BTRFS Auto Snapshotter (Daily) # 4 | # # 5 | # Automatically takes snapshots of / into # 6 | # /snaps/daily/YYYY-mm-dd # 7 | # every day -and- deletes all the previous day's HOURLY snapshots. # 8 | # # 9 | # Part of HopeSeekr's BashScripts Collection # 10 | # https://github.com/hopeseekr/BashScripts/ # 11 | # # 12 | # Copyright © 2020 Theodore R. Smith # 13 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 14 | # # 15 | # License: Creative Commons Attribution v4.0 International # 16 | ######################################################################### 17 | 18 | # @see https://serverfault.com/a/37836/56309 19 | if [[ $(/usr/bin/id -u) -ne 0 ]]; then 20 | echo "Error: You *MUST* run this as sudo or root!" 21 | echo "" 22 | exit 99 23 | fi 24 | 25 | mkdir -p /snaps/daily 26 | #btrfs subvolume snapshot / /snaps/daily/$(date +"%Y-%m-%d") 27 | 28 | DATE=$(date +"%Y-%m-%d") 29 | YESTERDAY=$(date --date=yesterday +"%Y-%m-%d") 30 | 31 | SUBVOLUMES=$(btrfs subvol list / | grep @snapshots/daily/${YESTERDAY}/[0-9]* | awk '{ print $9 }' | sed -e 's/@snapshots/\/snaps/') 32 | 33 | for snapshot in ${SUBVOLUMES}; do 34 | btrfs subvolume delete --commit-after ${snapshot} 35 | done 36 | -------------------------------------------------------------------------------- /ssh-autologin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ########################################################################## 3 | # ssh-autologin: Automatically configure .ssh/authorized_keys # 4 | # # 5 | # Automatically sets up (if needed) a SSH private key and installs it # 6 | # on the remote server (creating the .ssh, if needed, as well. # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2012-2021 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | # # 16 | # Run like ./ssh-autologin # 17 | # # 18 | ########################################################################## 19 | 20 | 21 | if [ -z "$1" ]; then 22 | echo "Error: You must put a destination server just like ssh [user@]destination-server" 23 | exit 1 24 | fi 25 | 26 | if [ ! -f $HOME/.ssh/id_rsa ]; then 27 | ssh-keygen -t rsa -b 4096 28 | fi 29 | 30 | scp $1:~/.ssh/id_rsa.pub 31 | ssh $1 'if [ ! -d .ssh ]; then mkdir .ssh; chmod 0700 .ssh; fi' 32 | cat ~/.ssh/id_rsa.pub | ssh $1 'cat >> .ssh/authorized_keys' 33 | 34 | echo "SSH auth has been set up. Please try to log in." 35 | 36 | -------------------------------------------------------------------------------- /ssh-keyphrase-only-once.installer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Only type in your SSH keyphrase once per boot. # 4 | # # 5 | # Installs a script and configures OpenSSH so that you are only # 6 | # prompted for your SSH keyphrase *once* per system boot. # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2020 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | ######################################################################### 16 | 17 | # Bail out if ~/.bash_profile already has the code. 18 | # @see https://stackoverflow.com/a/4749368/430062 19 | if ! grep -Fq "Run ssh-agent ONCE" ~/.bash_profile; then 20 | cat <<'LAUNCH_SSH_AGENT' >> ~/.bash_profile 21 | 22 | # Run ssh-agent ONCE per user per system boot. 23 | # [installed via https://github.com/hopeseekr/BashScripts] 24 | # @see https://unix.stackexchange.com/a/217223/15780 25 | if [ ! -S ~/.ssh/ssh_auth_sock ]; then 26 | eval `ssh-agent` 27 | ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock 28 | fi 29 | export SSH_AUTH_SOCK=~/.ssh/ssh_auth_ 30 | LAUNCH_SSH_AGENT 31 | fi 32 | 33 | if ! grep -Fqx "AddKeysToAgent yes" ~/.ssh/config ; then 34 | echo "" >> ~/.ssh/config 35 | echo "AddKeysToAgent yes" >> ~/.ssh/config 36 | fi 37 | 38 | source ~/.bash_profile 39 | -------------------------------------------------------------------------------- /cron.hourly/btrfs-snapshot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # BTRFS Auto Snapshotter (Hourly) # 4 | # # 5 | # Automatically takes snapshots of / into # 6 | # /snaps/daily/YYYY-mm-dd/HH # 7 | # every hour. # 8 | # # 9 | # Part of HopeSeekr's BashScripts Collection # 10 | # https://github.com/hopeseekr/BashScripts/ # 11 | # # 12 | # Copyright © 2020 Theodore R. Smith # 13 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 14 | # # 15 | # License: Creative Commons Attribution v4.0 International # 16 | ######################################################################### 17 | 18 | # @see https://serverfault.com/a/37836/56309 19 | if [[ $(/usr/bin/id -u) -ne 0 ]]; then 20 | echo "Error: You *MUST* run this as sudo or root!" 21 | echo "" 22 | exit 99 23 | fi 24 | 25 | DATE=$(date +"%Y-%m-%d") 26 | HOUR=$(date +"%H") 27 | 28 | mkdir -p /snaps/daily 29 | 30 | if [[ ! -d "/snaps/daily" ]]; then 31 | mkdir /snaps/daily 32 | fi 33 | 34 | if [[ ! -d "/snaps/daily/${DATE}" ]]; then 35 | btrfs subvolume snapshot / /snaps/daily/${DATE} 36 | fi 37 | 38 | if [[ ! -d "/snaps/code" ]]; then 39 | mkdir /snaps/code 40 | fi 41 | 42 | if [[ ! -d "/snaps/code/${DATE}" ]]; then 43 | btrfs subvolume snapshot /code /snaps/code/${DATE} 44 | fi 45 | 46 | 47 | btrfs subvolume snapshot /code /snaps/code/${DATE}/${HOUR} 48 | -------------------------------------------------------------------------------- /esoteric/arch-pacman-dupe-cleaner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ######################################################################### 4 | # Arch Linux Pacman Database de-Duplicator # 5 | # # 6 | # Remove duplicated Arch Linux Pacman entries. # 7 | # # 8 | # Inspired by https://unix.stackexchange.com/a/615578/15780 # 9 | # # 10 | # Part of HopeSeekr's BashScripts Collection # 11 | # https://github.com/hopeseekr/BashScripts/ # 12 | # # 13 | # Copyright © 2020 Theodore R. Smith # 14 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 15 | # # 16 | # License: Creative Commons Attribution v4.0 International # 17 | ######################################################################### 18 | 19 | sudo echo EUID: $EUID 20 | 21 | # 0. Login as root. 22 | if (($EUID != 0)); then 23 | echo "Please run as root" 24 | exit 25 | fi 26 | 27 | # 1. Remove the latest dupes from your Pacman DB. 28 | pacman -Syu 2>&1 | grep "duplicated database entry" > /tmp/dupes 29 | for latest in $(for dupe in $(cat /tmp/dupes | awk '{print $5}' | sed "s/'//g"); do 30 | ls /var/lib/pacman/local/$dupe* -d | tail -n1; done); do 31 | rm -rvf $latest; 32 | done 33 | rm /tmp/dupes 34 | 35 | # 2. Remove system of dupe files. 36 | pacman -Syu 2>&1 > /tmp/update-dupes 37 | cat /tmp/update-dupes | grep "exists in file system" | awk '{print $2}' | xargs rm -vf 38 | rm /tmp/update-dupes 39 | 40 | # 3. Reinstall everything (this will restore anything deleted in #2.). 41 | pacman -Syu 42 | 43 | -------------------------------------------------------------------------------- /stream-to-youtube: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # YouTube CLI Livestream Screencaster # 4 | # # 5 | # Live Screencast directly to YouTube from the CLI (via ffmpeg). # 6 | # # 7 | # You need to edit the file with your key and start the stream via # 8 | # YouTube's Creator Studio, first. # 9 | # # 10 | # Part of HopeSeekr's BashScripts Collection # 11 | # https://github.com/hopeseekr/BashScripts/ # 12 | # # 13 | # Copyright © 2020 Theodore R. Smith # 14 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 15 | # # 16 | # License: Creative Commons Attribution v4.0 International # 17 | ######################################################################### 18 | 19 | VBR="2500k" # Bitrate de la vidéo en sortie 20 | FPS="25" # FPS de la vidéo en sortie 21 | QUAL="medium" # Preset de qualité FFMPEG 22 | YOUTUBE_URL="rtmp://a.rtmp.youtube.com/live2" # URL de base RTMP youtube 23 | 24 | SOURCE="udp://239.255.139.0:1234" # Source UDP (voir les annonces SAP) 25 | KEY="xxxx-xxxx-xxxx-xxxx" # Clé à récupérer sur l'event youtube 26 | 27 | ffmpeg \ 28 | -i "$SOURCE" -deinterlace \ 29 | -vcodec libx264 -pix_fmt yuv420p -preset $QUAL -r $FPS -g $(($FPS * 2)) -b:v $VBR \ 30 | -acodec libmp3lame -ar 44100 -threads 6 -qscale 3 -b:a 712000 -bufsize 512k \ 31 | -f flv "$YOUTUBE_URL/$KEY" 32 | -------------------------------------------------------------------------------- /x265.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ######################################################################### 4 | # x265.sh: Rapid x265 HEVC Transcoder # 5 | # # 6 | # Transcode to h265 HEVC via the Intel graphics card using VAAPI # 7 | # with acceptable video quality loss for ~66-70% better compression. # 8 | # # 9 | # Part of HopeSeekr's BashScripts Collection # 10 | # https://github.com/hopeseekr/BashScripts/ # 11 | # # 12 | # Copyright © 2020 Theodore R. Smith # 13 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 14 | # # 15 | # License: Creative Commons Attribution v4.0 International # 16 | ######################################################################### 17 | 18 | mkdir -p x265 19 | VIDEO=${1%.*} 20 | #time ffmpeg -y -hide_banner -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -i "$1" -c:v libx265 -preset faster -x265-params crf=27 -c:a copy "x265/${VIDEO}.mkv" 21 | #time ffmpeg -y -hide_banner -hwaccel vaapi -vaapi_device /dev/dri/renderD128 -hwaccel_output_format vaapi -i "$1" -c:v hevc_qsv -preset slower -x265-params crf=27 -c:a copy 22 | 23 | # For "${@:2}" @see https://stackoverflow.com/a/9057392/430062 24 | 25 | #time LIBVA_DRIVER_NAME=iHD ffmpeg -y -hwaccel qsv -qsv_device /dev/dri/renderD128 -hwaccel_output_format qsv -i "$1" -c:v hevc_qsv -b:v 2500k -maxrate 6000k -bufsize 1200 -c:a copy "${@:2}" "x265/${VIDEO}.mkv" 26 | time LIBVA_DRIVER_NAME=iHD ffmpeg -y -hwaccel vaapi -vaapi_device /dev/dri/renderD128 -hwaccel_output_format vaapi -i "$1" -c:v hevc_vaapi -b:v 2500k -maxrate 6000k -bufsize 1200 -c:a copy "${@:2}" "x265/${VIDEO}.mkv" 27 | if [ "$?" -eq 0 ]; then 28 | mkdir -p x264 29 | mv "$1" x264 30 | fi 31 | 32 | -------------------------------------------------------------------------------- /git-same-sig-time: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ############################################################################# 3 | # git-same-sig-time: Unifies the GPG signature time with the commit's time. # 4 | # # 5 | # Part of HopeSeekr's BashScripts Collection # 6 | # https://github.com/hopeseekr/BashScripts/ # 7 | # # 8 | # Copyright © 2024 Theodore R. Smith # 9 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 10 | # # 11 | # License: Creative Commons Attribution v4.0 International # 12 | # # 13 | ############################################################################# 14 | 15 | # Verify faketime is installed 16 | if ! command -v faketime &> /dev/null 17 | then 18 | echo "'faketime' could not be found. Please install it first." 19 | echo " Ubuntu/Debian: sudo apt-get install faketime" 20 | echo " macOS: brew install libfaketime" 21 | exit 1 22 | fi 23 | 24 | git pretty -1 > /dev/null 2>&1 25 | if [ "$?" -eq 1 ]; then 26 | echo "Error: You need \`git pretty\` installed. Add this to your ~/.gitconfig:" 27 | echo "" 28 | echo " [pretty]" 29 | echo " shortlog = format:%C(auto,yellow)%h%C(auto,magenta)% G? %Cred%ad %C(auto,green)%<(20,trunc)%aN%C(auto,reset)%s%C(auto,red)% gD% D" 30 | exit 1 31 | fi 32 | 33 | # Get the count of git commits including and after the target commit. 34 | COMMIT_COUNT=$(git log --oneline "$1"^.. | wc -l) 35 | echo "Commit count: $COMMIT_COUNT" 36 | 37 | GIT_SEQUENCE_EDITOR="sed -i -re 's/^pick/edit/'" git rebase -i "$1"^ 38 | 39 | for a in $(eval echo {1.."$COMMIT_COUNT"}); do 40 | faketime "$(git pretty -1 | awk '{print $3" "$4" "$5}')" git commit --amend -S --no-edit 41 | git rebase --continue 42 | done 43 | -------------------------------------------------------------------------------- /bash-timer/README.md: -------------------------------------------------------------------------------- 1 | ## Bash Timer 2 | 3 | Shows a human-readable execution time for every command run in bash. 4 | 5 | You might also find my [**Amazing Linux, PHP, and Git Aliases**](https://gist.github.com/hopeseekr/fb85b7a179e3b9c97212925a2bd8400b) quite useful as well. 6 | 7 | The time will show up in the bottom left, immediately left of your `$PS1`. 8 | 9 | ![bash-timer image](https://user-images.githubusercontent.com/1125541/93687425-7c392100-fa83-11ea-9d36-cacbe03cc725.png) 10 | ``` 11 | 2 days 05:02:11.33 # A very long process 12 | ``` 13 | 14 | ## Installation 15 | 16 | ### Use the Installer 17 | 18 | ```bash 19 | curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.5.0/install | bash 20 | ``` 21 | 22 | ### Manual 23 | 24 | 1. Download the files: 25 | 26 | ```bash 27 | curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.5.0/bash-timer.sh -o $HOME/.bash-timer.sh 28 | echo "c9cf58a86712eb7360e08a072d03e53548e5e362944ab651aa71ee2a7846d22f $HOME/.bash-timer.sh" | sha256sum -c - 29 | 30 | curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.5.0/assets/bash-preexec.sh -o $HOME/.bash-preexec.sh 31 | echo "d512aa6043d69d636f0db711aab1675cc7c49b39da9ae58afcfb916dca8c4464 $HOME/.bash-preexec.sh" | sha256sum -c - 32 | ``` 33 | 34 | 2. Add the following to the very bottom of your `~/.bashrc`. 35 | 36 | ```bash 37 | # Bash Timer 38 | # See https://github.com/hopeseekr/bash-timer 39 | [[ -f ~/.bash-timer.sh ]] && source ~/.bash-timer.sh 40 | 41 | # See https://github.com/rcaloras/bash-preexec 42 | # **WARNING:** This must be the last line of your .bashrc. 43 | # Source our file at the end of our bash profile (e.g. ~/.bashrc, ~/.profile, or ~/.bash_profile) 44 | [[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh 45 | ``` 46 | ## License 47 | 48 | This project is licensed under the [Creative Commons Attribution License v4.0 International](LICENSE.cc-by.md). 49 | 50 | ![License Summary](https://user-images.githubusercontent.com/1125541/93617603-cd6de580-f99b-11ea-9da4-f79c168c97df.png) 51 | 52 | ## Contributors 53 | 54 | [Theodore R. Smith](https://www.phpexperts.pro/]) 55 | GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 56 | CEO: PHP Experts, Inc. 57 | -------------------------------------------------------------------------------- /bash-timer/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## Bash Timer 3 | ## https://github.com/hopeseekr/bash-timer/ 4 | ## https://github.com/hopeseekr/BashScripts/ 5 | ## 6 | ## Copyright © 2020-2024 Theodore R. Smith 7 | 8 | # Pull down our file from GitHub and write it to our home directory as a hidden file. 9 | curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.5.0/bash-timer.sh -o $HOME/.bash-timer.sh 10 | 11 | if [ $(builtin type -P "sha256sum" 2>&1) ]; then 12 | echo "c9cf58a86712eb7360e08a072d03e53548e5e362944ab651aa71ee2a7846d22f $HOME/.bash-timer.sh" | sha256sum -c - 13 | if [ $? -ne 0 ]; then 14 | rm -v ~/.bash-timer.sh 15 | echo ERROR!! Invalid shasum for .bash-timer.sh !! 16 | echo ERROR!! It has been deleted. Please grab from https://github.com/hopeseekr/bash-timer/ 17 | exit 1 18 | fi 19 | fi 20 | 21 | if [ ! -f ~/.bash-preexec.sh ]; then 22 | curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.5.0/assets/bash-preexec.sh -o $HOME/.bash-preexec.sh 23 | 24 | if [ $(builtin type -P "sha256sum" 2>&1) ]; then 25 | echo "d512aa6043d69d636f0db711aab1675cc7c49b39da9ae58afcfb916dca8c4464 $HOME/.bash-preexec.sh" | sha256sum -c - 26 | if [ $? -ne 0 ]; then 27 | rm -v ~/.bash-preexec.sh 28 | echo ERROR!! Invalid shasum for .bash-preexec.sh !! 29 | echo ERROR!! It has been deleted. Please grab from https://github.com/hopeseekr/bash-timer/ 30 | exit 2 31 | fi 32 | fi 33 | fi 34 | 35 | if [ ! -f "${HOME}/.bashrc" ]; then 36 | echo "ERROR: Could not find ~/.bashrc. You need to do a manual installation." 37 | echo "See https://github.com/hopeseekr/bash-timer" 38 | exit 3 39 | fi 40 | 41 | # Heredoc concatenation: https://stackoverflow.com/a/2954835/430062 42 | cat << EOF >> "${HOME}/.bashrc" 43 | # Bash Timer 44 | # See https://github.com/hopeseekr/bash-timer 45 | [[ -f ~/.bash-timer.sh ]] && source ~/.bash-timer.sh 46 | 47 | # See https://github.com/rcaloras/bash-preexec 48 | # **WARNING:** This must be the last line of your .bashrc. 49 | # Source our file at the end of our bash profile (e.g. ~/.bashrc, ~/.profile, or ~/.bash_profile) 50 | [[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh 51 | EOF 52 | 53 | echo "Install succeeded!" 54 | echo 55 | echo "Now run" 56 | echo " source ~/.bashrc" 57 | -------------------------------------------------------------------------------- /cron.daily/00_clear-cache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # The Amazing Linux Cache Purger # 4 | # # 5 | # Empties out the ~/.cache directory of every non-root user. # 6 | # Empties out Arch Linux' Pacman's package cache directory. # 7 | # # 8 | # This can optionally use nullfsvfs in order to limit wear-and-tear # 9 | # on SSDs. # 10 | # # 11 | # Saves over 4 GB of space on average on a 3-user system. # 12 | # # 13 | # Part of HopeSeekr's BashScripts Collection # 14 | # https://github.com/hopeseekr/BashScripts/ # 15 | # # 16 | # Copyright © 2020 Theodore R. Smith # 17 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 18 | # # 19 | # License: Creative Commons Attribution v4.0 International # 20 | ######################################################################### 21 | 22 | mkdir -p /run/media/sinkhole 23 | 24 | ## Attempt to mount nullfsvfs 25 | ## @see https://github.com/abbbi/nullfsvfs 26 | modprobe nullfs 2>/dev/null 27 | mount -t nullfs none /run/media/sinkhole 2>/dev/null 28 | if [ "$?" -ne 0 ]; then 29 | echo "NOTICE: Running this without nullfsvfs is not recommended due to extended wear on SSDs." 30 | echo " See https://github.com/abbbi/nullfsvfs" 31 | fi 32 | 33 | rsync -rv --remove-source-files --exclude={composer,spotify,thumbnails} /home/*/.cache/ /run/media/sinkhole/ 34 | rm -rf /run/media/sinkhole/* 35 | for user in $(ls /home); do 36 | # Delete dangling symlinks. 37 | # @see https://unix.stackexchange.com/a/49470/15780 38 | find /home/$user/.cache/ -type l -xtype l -delete 39 | # Delete empty directories. 40 | find /home/$user/.cache/ -type d -empty -delete 41 | done 42 | 43 | rm -rvf /var/cache/pacman/pkg/* 44 | -------------------------------------------------------------------------------- /tar-stats-src/help.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # tar-stats: A 'tar' Wrapper for Creation and Extraction # 4 | # # 5 | # A transparent wrapper for 'tar' to add progress bars for both creating# 6 | # and extracting archives. # 7 | # # 8 | # This script intercepts 'tar' commands to provide progress bars using # 9 | # 'pv' (Pipe Viewer), without changing the original 'tar' command # 10 | # syntax. It is fully portable and handles tar's flexible argument # 11 | # style, including the traditional dash-less format (e.g., 'tar cvf'). # 12 | # # 13 | # Part of HopeSeekr's BashScripts Collection # 14 | # https://github.com/hopeseekr/BashScripts/ # 15 | # # 16 | # Copyright © 2025 Theodore R. Smith # 17 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 18 | # # 19 | # License: Creative Commons Attribution v4.0 International # 20 | ######################################################################### 21 | 22 | show_help() { 23 | cat << 'EOF' 24 | Usage: $(basename "$0") [MODE] [OPTIONS] [files-or-dirs...] 25 | 26 | A transparent wrapper for 'tar' that enhances archive creation (-c) and 27 | extraction (-x) with progress bars using 'pv'. It accepts the same arguments 28 | as 'tar', including the traditional dash-less style. 29 | 30 | MODE (must be specified): 31 | -c, --create Create a new archive. 32 | -x, --extract Extract files from an archive. 33 | 34 | COMMON OPTIONS: 35 | -f, --file Required. The name of the archive file. 36 | -v, --verbose List files as they are processed. 37 | -C, --directory For extraction, change to this directory first. 38 | -z, -j, -J, -a Compression flags (optional for extraction). 39 | 40 | Examples: 41 | Create: $(basename "$0") -czvf my_app.tar.gz ./my_app 42 | Extract: $(basename "$0") -xvf my_app.tar.gz 43 | EOF 44 | } 45 | 46 | show_help 47 | -------------------------------------------------------------------------------- /changelog-maker-lite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ==== changelog-maker-lite ==== 3 | ######################################################################### 4 | # ChangeLog Maker (lite) # 5 | # # 6 | # Quickly creates CHANGELOG.md entries suitable for GitHub from # 7 | # the repo's commit log. # 8 | # # 9 | # It expects to be passed a git revision range, typically # 10 | # 'LAST_VERSION..' to get all commits since LAST_VERSION, # 11 | # or 'FIRST_VERSION..LAST_VERSION' for a specific range. # 12 | # If no argument is provided, it will generate a changelog for # 13 | # ALL commits in the repository. # 14 | # # 15 | # Part of HopeSeekr's BashScripts Collection # 16 | # https://github.com/hopeseekr/BashScripts/ # 17 | # # 18 | # Copyright © 2020 Theodore R. Smith # 19 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 20 | # # 21 | # License: Creative Commons Attribution v4.0 International # 22 | ######################################################################### 23 | 24 | # Determine the git log range. 25 | range="${1:-$(git rev-list --max-parents=0 HEAD 2>/dev/null)..HEAD}" 26 | 27 | # Use a hybrid approach for maximum reliability: 28 | # 1. `git log` formats the date/time string to avoid using `cut`. 29 | # 2. It also provides the Unix epoch timestamp (%at). 30 | # 3. A loop processes each line, using the reliable `date` command to get 31 | # the timezone abbreviation from the epoch timestamp. 32 | # This avoids the non-portable behavior of `%Z` inside `git log`. 33 | 34 | git log --date=format:'%Y-%m-%d %H:%M:%S' \ 35 | --pretty=format:'%at|%ad|%s' "$range" | \ 36 | while IFS='|' read -r epoch datetime subject || [[ -n "$subject" ]]; do 37 | # Skip potentially empty lines from git log's output 38 | if [[ -z "$epoch" ]]; then continue; fi 39 | 40 | tz_abbr=$(date -d "@$epoch" +"%Z") 41 | echo "* **[$datetime $tz_abbr]** $subject" 42 | done 43 | -------------------------------------------------------------------------------- /tar-sorted: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ############################################################################# 3 | # tar-sorted: Create tar files automatically sorted by file name. # 4 | # # 5 | # This is particularly helpful on random-order file systems, such as ext4. # 6 | # # 7 | # With the Bettergist Collector, we use this to be able to roughly estimate # 8 | # how long compressing and extracting multiple-gigabyte files with millions # 9 | # of files will take. # 10 | # # 11 | # It is a drop-in replacement for `tar`, and uses the same arguments. # 12 | # # 13 | # Optionally, you can install the function directly into your ~/.bashrc. # 14 | # # 15 | # Part of HopeSeekr's BashScripts Collection # 16 | # https://github.com/hopeseekr/BashScripts/ # 17 | # # 18 | # Copyright © 2024 Theodore R. Smith # 19 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 20 | # # 21 | # License: Creative Commons Attribution v4.0 International # 22 | # # 23 | ############################################################################# 24 | 25 | # Create tar archives sorted alphanumerically. 26 | 27 | function tar-sorted() { 28 | local options="$@" 29 | local archive="$2" 30 | local zstd_flag="" 31 | 32 | # Check for compression flag 33 | if [[ "$options" == *"--zstd"* ]]; then 34 | zstd_flag="--zstd" 35 | echo "Options (before): $options" 36 | options="${options/--zstd/}" 37 | echo "Options (after): $options" 38 | 39 | set -- $options 40 | fi 41 | 42 | # Only pass the remaining arguments (targets) to the find command 43 | find "${@:3}" -print0 | sort -z | tar "$zstd_flag" "$1" "$2" --no-recursion --null -T -; 44 | } 45 | 46 | function tar-sorted-orig() { 47 | find "${@:3}" -print0 | sort -z | tar "$1" "$2" --no-recursion --null -T -; 48 | }; 49 | 50 | 51 | tar-sorted "$@" 52 | -------------------------------------------------------------------------------- /gitconfig: -------------------------------------------------------------------------------- 1 | ######################################################################### 2 | # High-Octane Git Config # 3 | # # 4 | # A whole bunch of git aliases and configurations that greatly # 5 | # increase my own proficiency with git on the command line (CLI). # 6 | # # 7 | # Part of HopeSeekr's BashScripts Collection # 8 | # https://github.com/hopeseekr/BashScripts/ # 9 | # # 10 | # Copyright © 2020-2021 Theodore R. Smith # 11 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 12 | # # 13 | # License: Creative Commons Attribution v4.0 International # 14 | ######################################################################### 15 | [url "git@github.com:"] 16 | insteadOf = git://github.com/ 17 | insteadOf = https://github.com/ 18 | insteadOf = https://github.com/ 19 | [commit] 20 | gpgSign = true 21 | [alias] 22 | shows = show --show-signature 23 | # Tired of GitHub showing 20 commits over 10 days as all happening today when you do a rebase? 24 | # Run `git redate ` and it'll fix that. 25 | redate = rebase --committer-date-is-author-date 26 | resign = rebase --exec 'git commit --amend --no-edit -n -S' -i 27 | cloneup = clone --origin upstream 28 | c = checkout 29 | cp = cherry-pick 30 | cpm = cherry-pick -m1 31 | ll = log --pretty=shortlog --date=iso 32 | pretty = ll 33 | fix = rebase -i HEAD~2 34 | ego = commit --amend --reuse-message=HEAD --author \"Theodore R. Smith \" 35 | alterego = commit --amend --reuse-message=HEAD --author \"Theodore R. Smith \" 36 | alterSign = config user.signingKey \"B02DF5EE699DBE4149C922D359E310F3D9BC31BD\" 37 | # By Jared Knipp. @see https://stackoverflow.com/a/33760160/430062 38 | retag = "!f() { git tag $2 $1 && echo Tagged $2 at $1 && git tag -d $1; }; f" 39 | 40 | [pretty] 41 | shortlog = format:%C(auto,yellow)%h%C(auto,magenta)% G? %Cred%ad %C(auto,green)%<(20,trunc)%aN%C(auto,reset)%s%C(auto,red)% gD% D 42 | 43 | [url "https://"] 44 | insteadOf = git:// 45 | [pull] 46 | rebase = true 47 | 48 | # Automatically time out git when websites are not reachable. 49 | [http] 50 | lowSpeedTime = 20 51 | lowSpeedLimit = 1000 52 | 53 | -------------------------------------------------------------------------------- /git-commit-at-modded-time: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### ==== git-commit-at-modded-time ==== 3 | ########################################################################## 4 | # git-commit-at-modded-time: Use a file's modified time as the git time. # 5 | # # 6 | # Example: # 7 | # # 8 | # $ ls -l american-date # 9 | # #-rwxrwxr-x+ 1 1MB Oct 14 2020 american-date # 10 | # $ ./git-commit-at-modded-time american-date # 11 | # $ git pretty american-date # 12 | # 7462b66 G 2020-10-14 15:53:34 -0500 Theodore R. Smith # 13 | # # 14 | # Part of HopeSeekr's BashScripts Collection # 15 | # https://github.com/hopeseekr/BashScripts/ # 16 | # # 17 | # Copyright © 2023-2024 Theodore R. Smith # 18 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 19 | # # 20 | # License: Creative Commons Attribution v4.0 International # 21 | # # 22 | ########################################################################## 23 | 24 | 25 | if ! command -v faketime &> /dev/null 26 | then 27 | echo "'faketime' could not be found. Please install it first." 28 | exit 29 | fi 30 | 31 | export EDITOR=vim 32 | 33 | # Find the newest modified date among all files 34 | NEWEST_TIMESTAMP=0 35 | for file in "$@"; do 36 | if [ -f "$file" ]; then 37 | FILE_TIMESTAMP=$(stat -c %Y "$file") 38 | if [ "$FILE_TIMESTAMP" -gt "$NEWEST_TIMESTAMP" ]; then 39 | NEWEST_TIMESTAMP=$FILE_TIMESTAMP 40 | fi 41 | fi 42 | done 43 | 44 | if [ "$NEWEST_TIMESTAMP" -eq 0 ]; then 45 | echo "No valid files provided." 46 | exit 1 47 | fi 48 | 49 | # Convert timestamp to required formats 50 | MODDED_AT=$(date -d @"$NEWEST_TIMESTAMP" '+%Y-%m-%d %H:%M:%S %Z') 51 | COMMIT_TIME=$(date -d @"$NEWEST_TIMESTAMP" '+%A %-d %B %Y %-H:%M:%S %Z') 52 | 53 | # Add all files 54 | for file in "$@"; do 55 | if [ -f "$file" ]; then 56 | git add "$file" 57 | fi 58 | done 59 | 60 | # Commit once with the newest timestamp 61 | faketime "${MODDED_AT}" git commit -m "Modded @ ${COMMIT_TIME}." -e 62 | 63 | # Mexico = Extremely hot in the summer. 64 | # Panama = Hot in the summer. 65 | # Colombia = Great year-round. 66 | 67 | # Jan Feb March April (4) [mexico], 68 | # May June July August [colombia] (4) 69 | # Sep [panama] (1), 70 | # Oct Nov [mexico] (2), 71 | # Dec [colombia] (1) 72 | -------------------------------------------------------------------------------- /esoteric/arch-upgrade-postgres: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Arch Linux Upgrade Postgres # 4 | # # 5 | # Facilitates the upgrading of PostgreSQL on Arch Linux systems. # 6 | # # 7 | # Part of HopeSeekr's BashScripts Collection # 8 | # https://github.com/hopeseekr/BashScripts/ # 9 | # # 10 | # Copyright © 2025 Theodore R. Smith # 11 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 12 | # # 13 | # License: Creative Commons Attribution v4.0 International # 14 | ######################################################################### 15 | 16 | function is_root() 17 | { 18 | [ "$EUID" -eq 0 ]; 19 | } 20 | 21 | if ! is_root; then 22 | echo "Error: Root is required." 23 | exit 1 24 | fi 25 | 26 | pacman --noconfirm -Sy postgresql-old-upgrade 27 | 28 | cd /var/lib/postgres 29 | 30 | if [ $(stat -f --format=%T .) == 'btrfs' ]; then 31 | echo "Looks like a BTRFS file system..." 32 | echo "Making a snapshot of /var/lib/postgres/data in /var/lib/postgres/@data.backup" 33 | btrfs subvolume create /var/lib/postgres/@data.backup 34 | cp --reflink -af /var/lib/postgres/data/* /var/lib/postgres/@data.backup 35 | fi 36 | 37 | echo "Upgrading Postgres data..." 38 | 39 | mv /var/lib/postgres/data /var/lib/postgres/olddata 40 | mkdir /var/lib/postgres/data /var/lib/postgres/tmp 41 | chown postgres:postgres /var/lib/postgres/data /var/lib/postgres/tmp 42 | 43 | # As postgres user 44 | # Starting and stopping it fixes the error: 45 | # The source cluster was not shut down cleanly, state reported as: "in production" 46 | 47 | sudo -u postgres bash < # 17 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 18 | # # 19 | # License: Creative Commons Attribution v4.0 International # 20 | ######################################################################### 21 | 22 | $sanity = function (string $username) { 23 | if (empty($username)) { 24 | echo "Error: Please provide a GitHub username or organization.\n"; 25 | exit(1); 26 | } 27 | 28 | if (!preg_match('/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i', $username)) { 29 | echo "Error: The GitHub username you provided contains invalid characters.\n"; 30 | exit(2); 31 | } 32 | }; 33 | 34 | $username = $argv[1]; 35 | $sanity($username); 36 | 37 | ini_set('user_agent', 'github repo downloader'); 38 | 39 | // From: https://stackoverflow.com/a/2280413/430062 40 | function checkURL(string $url): bool 41 | { 42 | if (function_exists('curl_init')) { 43 | return curl_init($url) !== false; 44 | } 45 | $headers = @get_headers($url); 46 | 47 | return (!(!$headers || $headers[0] != 'HTTP/1.1 200 OK')); 48 | }; 49 | 50 | function grabUserJson(string $username) 51 | { 52 | $url = "https://api.github.com/users/$username/repos?per_page=200"; 53 | if (checkURL($url) === false) { 54 | throw new \RuntimeException("Could not fetch GitHub repo info for a user named '$username'."); 55 | } 56 | 57 | return json_decode(file_get_contents($url)); 58 | } 59 | 60 | function grabOrgJson(string $orgname) 61 | { 62 | $url = "https://api.github.com/orgs/$orgname/repos?per_page=200&q=is_fork:false"; 63 | if (checkURL($url) === false) { 64 | throw new \RuntimeException("Could not fetch GitHub repo info for either a user or organization named '$orgname'."); 65 | } 66 | 67 | return json_decode(file_get_contents($url)); 68 | } 69 | 70 | try { 71 | $repos = grabUserJson($username); 72 | } catch (\RuntimeException $e) { 73 | $repos = grabOrgJson($username); 74 | } 75 | 76 | foreach ($repos as $repo) { 77 | if ($repo->fork === 1 || empty($repo->clone_url)) { 78 | continue; 79 | } 80 | 81 | system("git clone {$repo->clone_url}"); 82 | } 83 | -------------------------------------------------------------------------------- /git-mtime: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ########################################################################## 3 | # git-mtime: Git MTime Restorer # 4 | # # 5 | # Restores each file's modification time in your working directory # 6 | # to when it was last updated in the remote git repository. # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2012-2020 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | # # 16 | # When you checkout a repository & run this, your workdir goes from: # 17 | # # 18 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 changelog-maker-lite* # 19 | # -rw-rw-r--+ 1 tsmith users 1MB Oct 22 01:58 CHANGELOG.txt # 20 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 clone-github-repos.php* # 21 | # drwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 cron.daily/ # 22 | # drwxr-xr-x+ 1 tsmith users 1MB Oct 22 01:58 cron.hourly/ # 23 | # -rw-rw-r--+ 1 tsmith users 1MB Oct 22 01:58 gitconfig # 24 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 git-mtime* # 25 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 init-btrfs-rootfs* # 26 | # # 27 | # To: # 28 | # # 29 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 1 08:38 changelog-maker-lite* # 30 | # -rw-rw-r--+ 1 tsmith users 1MB Oct 22 01:01 CHANGELOG.txt # 31 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 1 00:23 clone-github-repos.php* # 32 | # drwxrwxr-x+ 1 tsmith users 1MB Oct 1 08:18 cron.daily/ # 33 | # drwxr-xr-x+ 1 tsmith users 1MB Oct 1 08:18 cron.hourly/ # 34 | # -rw-rw-r--+ 1 tsmith users 1MB Oct 1 01:10 gitconfig # 35 | # -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:01 git-mtime* # 36 | # -rwxrwxr-x+ 1 tsmith users 1MB Sep 30 23:19 init-btrfs-rootfs* # 37 | # # 38 | ########################################################################## 39 | 40 | MTIME_DATA=$(git log --pretty=%at --name-status --reverse) 41 | 42 | for FILE in $MTIME_DATA; do 43 | # See if it's an integer. If so, it's our mtime. 44 | # CAVEAT: Won't work for files with mtimes before 2001-09-09 UTC. 45 | # CAVEAT: Won't behave properly if there are root-level files exactly 46 | # equivalent to epoch times after 2001-09-09 UTC. 47 | # @see https://stackoverflow.com/a/808740/430062 48 | if [ "$FILE" -eq "$FILE" ] 2> /dev/null && [ "$FILE" -ge 1000000000 ] 2> /dev/null; then 49 | MTIME=$FILE 50 | else 51 | if [ -f "$FILE" ]; then 52 | # Change the modification date to the epoch time. 53 | # @see https://unix.stackexchange.com/a/139674/15780 54 | touch --date="@${MTIME}" "${FILE}" 55 | 56 | # Don't forget the directories, too... 57 | touch --date="@${MTIME}" "$(dirname "${FILE}"})" 58 | fi 59 | fi 60 | done 61 | 62 | -------------------------------------------------------------------------------- /turn-off-monitors: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # Turn Monitors Off via the Command Line (CLI) # 4 | # # 5 | # Does exactly what it says. It will turn off all of your monitors # 6 | # until the mouse is moved or a key is pressed. # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2020-2024 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | ######################################################################### 16 | 17 | function turn-off-screen-in-wayland() 18 | { 19 | echo "Turning off monitors..." 20 | if [[ $XDG_SESSION_DESKTOP == gnome* ]]; then 21 | _trigger-monitors-off-in-gnome-wayland 22 | 23 | local interaction_event=$(wait_until_mouse_or_keyboard_event) 24 | echo "Event Type: ${interaction_event}" 25 | 26 | echo "Turning monitors back on..." 27 | _trigger-monitors-on-in-gnome-wayland 28 | 29 | elif [[ $XDG_SESSION_DESKTOP == "plasma" || $XDG_SESSION_DESKTOP == "KDE" ]]; then 30 | _trigger-monitors-off-in-kde-wayland 31 | else 32 | echo "ERROR: Unsupported Wayland Desktop Environment: $XDG_SESSION_DESKTOP" 33 | exit 2 34 | fi 35 | } 36 | 37 | function _trigger-monitors-off-in-gnome-wayland() 38 | { 39 | busctl --user set-property org.gnome.Mutter.DisplayConfig /org/gnome/Mutter/DisplayConfig org.gnome.Mutter.DisplayConfig PowerSaveMode i 1 40 | } 41 | 42 | function _trigger-monitors-on-in-gnome-wayland() 43 | { 44 | busctl --user set-property org.gnome.Mutter.DisplayConfig /org/gnome/Mutter/DisplayConfig org.gnome.Mutter.DisplayConfig PowerSaveMode i 0 45 | } 46 | 47 | function _trigger-monitors-off-in-kde-wayland() 48 | { 49 | kscreen-doctor --dpms off 50 | } 51 | 52 | ### BUILT FROM framework/wait_until_mouse_or_keyboard_event.sh 53 | function wait_until_mouse_or_keyboard_event() 54 | { 55 | # Ensure libinput-tools is installed 56 | if ! command -v libinput >/dev/null 2>&1; then 57 | echo "Error: libinput-tools is not installed. Please install it and try again." 58 | exit 1 59 | fi 60 | 61 | if ! command -v sudo >/dev/null 2>&1; then 62 | echo "Error: sudo is not installed. Please install it and try again." 63 | exit 1 64 | fi 65 | 66 | # Turn off the monitors. 67 | 68 | # Start monitoring input events 69 | echo "Monitoring input events. Press Ctrl+C to stop." 70 | sudo stdbuf -oL libinput debug-events | while read -r line; do 71 | case "$line" in 72 | *"KEYBOARD_KEY"*) 73 | echo "KEYBOARD_KEY"; return 0; 74 | ;; 75 | *"POINTER_BUTTON"*) 76 | echo "MOUSE_CLICK"; return 0; 77 | ;; 78 | *"POINTER_MOTION"*) 79 | echo "MOUSE_MOVED"; return 0; 80 | ;; 81 | *"TOUCH_FRAME"*) 82 | echo "TOUCH_FRAME"; return 0; 83 | ;; 84 | *"TOUCH_MOTION"*) 85 | echo "TOUCH_MOTION"; return 0; 86 | ;; 87 | esac 88 | done 89 | } 90 | ### END framework/wait_until_mouse_or_keyboard_event.sh 91 | 92 | if [ "$XDG_SESSION_TYPE" == "wayland" ]; then 93 | turn-off-screen-in-wayland 94 | else 95 | sleep 0.5; xset dpms force off 96 | fi 97 | -------------------------------------------------------------------------------- /bash-timer/bash-timer.sh: -------------------------------------------------------------------------------- 1 | ## Bash Timer 2 | ## https://github.com/hopeseekr/bash-timer/ 3 | ## https://github.com/hopeseekr/BashScripts/ 4 | ## 5 | ## Copyright © 2020-2024 Theodore R. Smith 6 | ## GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 7 | ## https://stackoverflow.com/users/story/430062 8 | ## 9 | ## Based off of the work of 10 | ## * https://jakemccrary.com/blog/2020/04/21/using-bash-preexec-for-monitoring-the-runtime-of-your-last-command/ 11 | ## 12 | ## This file is licensed under the Creative Commons Attribution License v4.0 International. 13 | ## 14 | ## Version 1.5.0 @ 2024-08-24. 15 | 16 | ## Set up tput for multi-terminal color support. 17 | if [ $(builtin type -P "tput" 2>&1) ]; then 18 | tput init 19 | BOLD="\[$(tput bold)\]" 20 | RESET="\[$(tput sgr0)\]" 21 | else 22 | BOLD="\[\033[1m\]" 23 | RESET="\[\033[0m\]" 24 | fi 25 | 26 | pad_number() { 27 | number="$1" 28 | while [ ${#number} -lt 3 ]; do 29 | number="0${number}" 30 | done 31 | echo "$number" 32 | } 33 | 34 | human_time() 35 | { 36 | local msg 37 | local s=$1 38 | local days=$((s / (60*60*24))) 39 | s=$((s - days*60*60*24)) 40 | local hours=$((s / (60*60))) 41 | s=$((s - hours*60*60)) 42 | local min=$((s / 60)) 43 | s=$((s - min*60)) 44 | 45 | if (($days > 0)); then 46 | printf "%1d:%02d:%02d:%02d" $days $hours $min $s 47 | return 48 | fi 49 | 50 | if (($hours > 0)); then 51 | printf "%1d:%02d:%02d" $hours $min $s 52 | return 53 | fi 54 | 55 | if (($min > 0)); then 56 | printf "%1d:%02d" $min $s 57 | return 58 | fi 59 | } 60 | 61 | bashtimer_preexec() { 62 | # Thanks to /u/OneTurnMore 63 | # https://www.reddit.com/r/bash/comments/ivz276/tired_of_typing_time_all_the_time_try_bashtimer/g5wui2l/ 64 | if [ ! -z "$EPOCHREALTIME" ]; then 65 | # Replace "," decimal separator with ".". This is needed for European locales, among others. 66 | E_TIME="${EPOCHREALTIME/,/.}" 67 | 68 | begin_s=${E_TIME%.*} 69 | begin_ns=${E_TIME#*.} 70 | begin_ns="${begin_ns#0}" 71 | else 72 | read begin_s begin_ns <<< $(date +"%s %N") 73 | fi 74 | timer_show="0" 75 | } 76 | 77 | bashtimer_precmd() { 78 | if [ ! -z "$begin_ns" ]; then 79 | local s 80 | local ms 81 | local end_s 82 | local end_ns 83 | 84 | # Thanks to /u/OneTurnMore 85 | # https://www.reddit.com/r/bash/comments/ivz276/tired_of_typing_time_all_the_time_try_bashtimer/g5wui2l/ 86 | if [ ! -z "$EPOCHREALTIME" ]; then 87 | # Replace "," decimal separator with ".". This is needed for European locales, among others. 88 | E_TIME="${EPOCHREALTIME/,/.}" 89 | 90 | end_s=${E_TIME%.*} 91 | # echo "Begin Seconds: $begin_s | End Seconds: $end_s" 92 | end_ns=${E_TIME#*.} 93 | end_ns="${end_ns#0}" 94 | 95 | # Convert strings with leading zeros to base 10 integers 96 | begin_ns=$((10#$begin_ns)) 97 | end_ns=$((10#$end_ns)) 98 | 99 | if [ $end_ns -lt $begin_ns ]; then 100 | end_ns=$((1000000 + $end_ns)) 101 | end_s=$(($end_s - 1)) 102 | fi 103 | 104 | s=$((end_s - begin_s)) 105 | if [ "$end_ns" -ge "$begin_ns" ]; then 106 | ms=$(( (end_ns - begin_ns) / 1000 )) 107 | else 108 | ms=$((((1000000 + end_ns) - begin_ns) / 1000)) 109 | fi 110 | 111 | # Ensure ms is always three digits for consistency in output: 112 | ms=$(pad_number "$ms") 113 | else 114 | # For Bash < v5.0 115 | read end_s end_ns <<< $(date +"%s %N") 116 | end_ns="${end_ns##+(0)}" 117 | 118 | s=$((end_s - begin_s)) 119 | if [ "$end_ns" -ge "$begin_ns" ] 120 | then 121 | ms=$(((end_ns - begin_ns) / 1000000)) 122 | else 123 | s=$((s - 1)) 124 | ms=$(((1000000000 + end_ns - begin_ns) / 1000000)) 125 | fi 126 | fi 127 | 128 | if (($s > 60)); then 129 | timer_show="$(human_time $s).$ms" 130 | else 131 | timer_show="$s.$ms " 132 | fi 133 | fi 134 | 135 | if [ -z "$PS1orig" ]; then 136 | PS1orig=$PS1 137 | else 138 | if [ -z "$NO_BASHTIMER" ]; then 139 | PS1="${BOLD}$timer_show${RESET}$PS1orig" 140 | fi 141 | fi 142 | } 143 | 144 | ## Using `bash-preexec` functions arrays makes this script compatible with other scripts that use `bash-preexec`. 145 | ## It avoids overwriting variables `preexec` and `precmd`. 146 | preexec_functions+=(bashtimer_preexec) 147 | precmd_functions+=(bashtimer_precmd) 148 | -------------------------------------------------------------------------------- /bash_rc.aliases: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ######################################################################### 4 | # High-Octane .bashrc aliase and functions # 5 | # # 6 | # These aliases and functions have revolutionized how I develop # 7 | # via the command line (CLI) and how I interact with Linux / UNIX. # 8 | # # 9 | # Part of HopeSeekr's BashScripts Collection # 10 | # https://github.com/hopeseekr/BashScripts/ # 11 | # # 12 | # Copyright © 2015-2020 Theodore R. Smith # 13 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 14 | # # 15 | # Copyright © 2016-2018 Rishi Ramawat https://github.com/rishi-ramawat # 16 | # # 17 | # License: Creative Commons Attribution v4.0 International # 18 | ######################################################################### 19 | 20 | # Ease-of-use Aliases 21 | alias ..="cd .." 22 | 23 | # Colorized grep and less! 24 | alias grep='grep --color=auto' 25 | alias less="less -r" 26 | 27 | alias ls="ls -CF --color=auto --block-size=MB" 28 | alias ll="ls -lisa --color=auto" 29 | alias lsl="ls -lhFA | less" 30 | alias mkdir="mkdir -pv" 31 | alias userlist="cut -d: -f1 /etc/passwd" 32 | alias psgrep="ps aux | grep -v grep | grep -i -e VSZ -e" 33 | alias wget="wget -c" 34 | # Instantly grabs your EXTERNAL IP from a 3rd party service. 35 | alias myip="curl http://ipecho.net/plain; echo" 36 | alias logs="find /var/log -type f -exec file {} \; | grep 'text' | cut -d' ' -f1 | sed -e's/:$//g' | grep -v '[0-9]$' | xargs tail -f" 37 | alias folders='find . -maxdepth 1 -type d -print0 | xargs -0 du -sk | sort -rn' 38 | 39 | ## Shows full ISO dates to the nanosecond: 40 | ## -rwxr-xr-x 1 tsmith tsmith 1MB 2020-10-01 00:48:49.759629612 -0500 bash_rc.aliases* 41 | ## -rwxr-xr-x 1 tsmith tsmith 1MB 2020-10-01 00:24:46.749613559 -0500 clone-github-repos.php* 42 | alias lsdate="ls -l --time-style=full-iso" 43 | 44 | # Creates an archive (*.tar.gz) from given directory. 45 | function maketar() { tar cvzf "${1%%/}.tar.gz" "${1%%/}/"; } 46 | 47 | # Create a ZIP archive of a file or folder. 48 | function makezip() { zip -r "${1%%/}.zip" "$1" ; } 49 | 50 | # create an directory and directly cd into it 51 | mcd () { 52 | mkdir -p $1 53 | cd $1 54 | } 55 | 56 | 57 | # Dev aliases [RR=Copyright by Rishi Ramawat] 58 | alias csup='(docker-compose up -d && docker-compose logs -ft)' 59 | alias art='php artisan' 60 | alias rdb='art migrate:refresh; art db:seed' 61 | alias pu='phpunit' 62 | alias puf='phpunit --filter=' 63 | alias puh='phpunit --coverage-html coverage' 64 | alias put='phpunit --testdox' 65 | alias gcp='git cherry-pick' 66 | alias grd='git rebase --committer-date-is-author-date' 67 | alias gpm='git pull --rebase origin master' 68 | alias gs='git show' 69 | alias nuke="find . -maxdepth 1 ! -name '.' ! -name '.git' -exec rm -rf {} \; && git reset --hard" 70 | alias behat='behat --format-settings='"'"'{"expand": true}'"'"'' 71 | alias gd='git diff' 72 | alias ga='git add' 73 | ## End copyright by Rishi Ramawata 74 | 75 | alias docker-prune='docker rmi $(docker images | grep "^" | awk "{print $3}")' 76 | alias psql="sudo -u postgres psql" 77 | alias gco='git checkout --ours' 78 | alias pf='php-cs-fixer fix' 79 | 80 | # System performance 81 | alias opti='optimus-manager --print-mode' 82 | alias super7="7z a -t7z -m0=lzma -mx=9 -mfb=64 -md=32m -ms=on" 83 | alias qmv='qmv --format=destination-only' 84 | 85 | # Video Archiving aliases 86 | # Part of the https://www.ourtube.me/ Platform. 87 | alias youtube-dl='youtube-dl -f best --external-downloader aria2c --external-downloader-args="-x8 -s8 -k 1M"' 88 | alias youtube-mp3="youtube-dl -x --audio-format mp3" 89 | alias youtube-archive='youtube-dl --external-downloader aria2c --external-downloader-args="-x8 -s8 -k 1M" --write-description -o "%(playlist_index)s-%(title)s::%(id)s.%(upload_date)s.%(ext)s" -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]"' 90 | # Get the video resolution [e.g., 1920x1080] 91 | alias vidres="mediainfo mediainfo --Inform='Video;%Width%x%Height%'" 92 | 93 | # Use Intel QSV for video hardware acceleration of VLC and mpv. 94 | export LIBVA_DRIVER_NAME=iHD 95 | alias mpv="mpv --hwdec=libva-intel-driver" 96 | 97 | # Show free memory in MB and total. 98 | alias free="free -mt" 99 | 100 | # Show a process PID's full date and time. 101 | # @see https://stackoverflow.com/a/5731337/430062 102 | alias ps-date='ps -eo pid,lstart,cmd' 103 | 104 | # Hide all of the /dev/loop snap devices from df: 105 | alias df='df -l -BM -Tx"squashfs"' 106 | 107 | # Make `watch` honor ~/.bashrc aliases. 108 | alias watch='watch ' 109 | 110 | # Replace ssh with mosh, if it is installed. 111 | if command -v mosh >/dev/null 2>&1; then 112 | alias ssh=mosh 113 | fi 114 | -------------------------------------------------------------------------------- /tests/test-tar-stats.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # test_tar-stats.sh - A test suite for the tar-stats script. 4 | # 5 | # This script sets up a temporary test environment, runs a series of tests 6 | # against the 'tar-stats' script with various argument styles, and reports 7 | # the success or failure of each test. 8 | # 9 | 10 | # --- Configuration --- 11 | TARP_SCRIPT="./tar-stats" 12 | TEST_DIR="_test_tar-stats_workdir" 13 | PASS_COUNT=0 14 | FAIL_COUNT=0 15 | 16 | # --- Helper Functions --- 17 | # A simple color system 18 | c_red="\033[1;31m" 19 | c_green="\033[1;32m" 20 | c_yellow="\033[1;33m" 21 | c_reset="\033[0m" 22 | 23 | log_pass() { 24 | echo -e " [${c_green}PASS${c_reset}] $1" 25 | ((PASS_COUNT++)) 26 | } 27 | 28 | log_fail() { 29 | echo -e " [${c_red}FAIL${c_reset}] $1" 30 | ((FAIL_COUNT++)) 31 | } 32 | 33 | log_info() { 34 | echo -e "${c_yellow}==>${c_reset} $1" 35 | } 36 | 37 | # --- Setup and Teardown --- 38 | setup() { 39 | log_info "Setting up test environment in '$TEST_DIR'..." 40 | rm -rf "$TEST_DIR" 41 | mkdir -p "$TEST_DIR" 42 | cd "$TEST_DIR" || exit 1 43 | 44 | # Create some test files and directories 45 | mkdir -p src/subdir 46 | dd if=/dev/urandom of=src/file1.bin bs=1K count=100 status=none 47 | echo "This is a test text file." > src/file2.txt 48 | echo "Another file in a subdirectory" > src/subdir/file3.txt 49 | ln -s src/file1.bin src/file1.link 50 | 51 | # Make sure tar-stats is executable 52 | if [[ ! -x "../$TARP_SCRIPT" ]]; then 53 | echo "Error: Script '$TARP_SCRIPT' not found or not executable." >&2 54 | exit 1 55 | fi 56 | } 57 | 58 | teardown() { 59 | log_info "Cleaning up test environment..." 60 | cd .. 61 | rm -rf "$TEST_DIR" 62 | } 63 | 64 | # --- Test Case Function --- 65 | # Usage: run_test "Test Name" "archive.name" "expected_content_file" 66 | run_test() { 67 | local test_name="$1" 68 | local archive_name="$2" 69 | local content_check="$3" 70 | shift 3 71 | local tar-stats_args=("$@") 72 | 73 | echo "Running Test: $test_name" 74 | 75 | # Run the tar-stats command 76 | "../$TARP_SCRIPT" "${tar-stats_args[@]}" > /dev/null 2>&1 77 | 78 | # Check 1: Archive file was created 79 | if [[ -f "$archive_name" ]]; then 80 | log_pass "Archive file '$archive_name' was created." 81 | else 82 | log_fail "Archive file '$archive_name' was NOT created." 83 | return 84 | fi 85 | 86 | # Check 2: Archive content is valid 87 | # We decompress and check the list of files 88 | local tar_list_cmd="" 89 | case "$archive_name" in 90 | *.tar.gz|*.tgz) tar_list_cmd="tar -tzf";; 91 | *.tar.bz2|*.tbz2) tar_list_cmd="tar -tjf";; 92 | *.tar.xz|*.txz) tar_list_cmd="tar -tJf";; 93 | *.tar) tar_list_cmd="tar -tf";; 94 | *) log_fail "Unknown archive type for content check."; return;; 95 | esac 96 | 97 | # Get the list of files from the created archive and sort them 98 | local archive_contents 99 | archive_contents=$($tar_list_cmd "$archive_name" 2>/dev/null | sort) 100 | 101 | if [[ "$archive_contents" == "$content_check" ]]; then 102 | log_pass "Archive content is correct." 103 | else 104 | log_fail "Archive content is INCORRECT." 105 | echo " Expected: $content_check" 106 | echo " Got: $archive_contents" 107 | fi 108 | } 109 | 110 | # --- Main Test Execution --- 111 | # Prepare the environment 112 | trap teardown EXIT 113 | setup 114 | 115 | # Define the expected contents of the archive (sorted) 116 | # This ensures all tests are validated against the same ground truth. 117 | EXPECTED_CONTENTS=$(tar -cf - src | tar -tf - | sort) 118 | 119 | log_info "Starting Tests..." 120 | 121 | # Test Case 1: Standard GNU/Linux style, gzip 122 | run_test "Standard gzip (-czf)" \ 123 | "test1.tar.gz" "$EXPECTED_CONTENTS" \ 124 | -czf test1.tar.gz src 125 | 126 | # Test Case 2: Traditional dash-less style, bzip2 127 | run_test "Dash-less bzip2 (cjf)" \ 128 | "test2.tar.bz2" "$EXPECTED_CONTENTS" \ 129 | cjf test2.tar.bz2 src 130 | 131 | # Test Case 3: Long options, xz 132 | # Skip if xz is not available 133 | if command -v xz &>/dev/null; then 134 | run_test "Long options xz (--create --xz --file)" \ 135 | "test3.tar.xz" "$EXPECTED_CONTENTS" \ 136 | --create --xz --file=test3.tar.xz src 137 | else 138 | log_info "Skipping xz test: 'xz' command not found." 139 | fi 140 | 141 | 142 | # Test Case 4: Separated flags, uncompressed 143 | run_test "Separated flags, no compression (-c -v -f)" \ 144 | "test4.tar" "$EXPECTED_CONTENTS" \ 145 | -c -v -f test4.tar src 146 | 147 | # Test Case 5: File flag first 148 | run_test "File flag first (-f ... -c)" \ 149 | "test5.tar.gz" "$EXPECTED_CONTENTS" \ 150 | -f test5.tar.gz -cz src 151 | 152 | # Test Case 6: Auto-compress (-a) 153 | run_test "Auto-compress flag (-a)" \ 154 | "test6.tar.xz" "$EXPECTED_CONTENTS" \ 155 | -caf test6.tar.xz src 156 | 157 | 158 | log_info "Test suite finished." 159 | echo -e "Results: ${c_green}${PASS_COUNT} Passed${c_reset}, ${c_red}${FAIL_COUNT} Failed${c_reset}." 160 | 161 | # Exit with a non-zero status if any tests failed 162 | if [[ $FAIL_COUNT -gt 0 ]]; then 163 | exit 1 164 | fi 165 | -------------------------------------------------------------------------------- /tar-stats-src/extract.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # tar-stats-extract: A 'tar' Wrapper for Archive Extraction # 4 | # # 5 | # Adds progress bars during extraction using 'pv'. Supports all common # 6 | # formats: .tar.gz, .tar.bz2, .tar.xz, .tar.zst, .tar.lzma. # 7 | # # 8 | # Based on tar-stats by Theodore R. Smith # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | ######################################################################### 11 | 12 | set -o pipefail 13 | 14 | # --- Help/Usage Function --- 15 | show_help() { 16 | cat << EOF 17 | Usage: $(basename "$0") [OPTIONS] [files-or-dirs...] 18 | 19 | Extract an archive with progress reporting. 20 | 21 | OPTIONS: 22 | -x, --extract (optional; required for this script to work) 23 | -f, --file Required. The archive filename. 24 | -v, --verbose List files as they are extracted. 25 | -C, --directory Change to this directory before extracting. 26 | -a, --auto Try to auto-detect compression based on extension. 27 | 28 | Examples: 29 | $(basename "$0") -xvf my_app.tar.gz 30 | $(basename "$0") -xvf my_app.tar.xz -C ./extracted/ 31 | EOF 32 | } 33 | 34 | # --- Argument Parsing & Validation --- 35 | parseargs() { 36 | archive_name="" 37 | input_paths=() 38 | passthrough_opts=() 39 | local has_file_flag=0 40 | local next_arg_is_file=0 41 | local next_arg_is_dir=0 42 | 43 | for arg in "$@"; do 44 | if [[ $next_arg_is_file -eq 1 ]]; then 45 | archive_name="$arg" 46 | next_arg_is_file=0 47 | continue 48 | fi 49 | if [[ $next_arg_is_dir -eq 1 ]]; then 50 | passthrough_opts+=("$arg") 51 | next_arg_is_dir=0 52 | continue 53 | fi 54 | 55 | case "$arg" in 56 | --file) has_file_flag=1; next_arg_is_file=1 ;; 57 | --file=*) has_file_flag=1; archive_name="${arg#*=}" ;; 58 | -f) has_file_flag=1; next_arg_is_file=1 ;; 59 | -C|--directory) passthrough_opts+=("$arg"); next_arg_is_dir=1 ;; 60 | -*) 61 | if [[ "$arg" =~ f ]]; then has_file_flag=1; next_arg_is_file=1; fi 62 | if [[ "$arg" =~ C ]]; then next_arg_is_dir=1; fi 63 | passthrough_opts+=("$arg") 64 | ;; 65 | *) 66 | # Check if this is a dash-less tar option (e.g., "cvf", "czf") 67 | if [[ $has_file_flag -eq 0 ]] && [[ "$arg" =~ ^[a-zA-Z]+$ ]] && [[ "$arg" =~ [cf] ]]; then 68 | # This looks like dash-less tar flags 69 | if [[ "$arg" =~ f ]]; then 70 | has_file_flag=1 71 | next_arg_is_file=1 72 | fi 73 | # Add dash and pass through 74 | passthrough_opts+=("-$arg") 75 | else 76 | input_paths+=("$arg") 77 | fi 78 | ;; 79 | esac 80 | done 81 | 82 | if [[ $has_file_flag -eq 0 || -z "$archive_name" ]]; then 83 | echo "Error: The -f or --file flag is required." >&2 84 | return 1 85 | fi 86 | 87 | if [[ ! -f "$archive_name" ]]; then 88 | echo "Error: Archive not found: '$archive_name'" >&2 89 | return 1 90 | fi 91 | } 92 | 93 | # --- Decompressor Selection --- 94 | get_decompressor() { 95 | local name="$1" 96 | local cmd="cat" 97 | 98 | case "$name" in 99 | *.tar.gz|*.tgz) cmd="gzip -dc" ;; 100 | *.tar.bz2|*.tbz2) cmd="bzip2 -dc" ;; 101 | *.tar.xz|*.txz) cmd="xz -dc" ;; 102 | *.tar.zst|*.tzst) cmd="zstd -dc" ;; 103 | *.tar.lzma|*.tlzma) cmd="xz -dc --format=lzma" ;; 104 | *) echo "Unknown extension; assuming raw tar." >&2 ;; 105 | esac 106 | 107 | echo "$cmd" 108 | } 109 | 110 | # --- Main Extraction Logic --- 111 | main() { 112 | if [[ $# -eq 0 ]] || [[ " $* " =~ (--help|-h) ]]; then show_help; exit 0; fi 113 | 114 | parseargs "$@" || exit 1 115 | 116 | # Get archive size 117 | local archive_size 118 | archive_size=$(stat -c %s "${archive_name}" 2>/dev/null || stat -f %z "${archive_name}" 2>/dev/null) 119 | if ! [[ "$archive_size" =~ ^[0-9]+$ ]] || [[ "$archive_size" -eq 0 ]]; then 120 | echo "Error: Could not determine archive size." >&2 121 | exit 1 122 | fi 123 | 124 | # Set decompressor 125 | local decompress_cmd 126 | decompress_cmd=$(get_decompressor "$archive_name") 127 | 128 | # Cleanup options: strip compression flags 129 | local clean_opts 130 | clean_opts=$(echo "$*" | tr ' ' '\n' | grep -vE -- '-[zjJZa]|--(?:gzip|bzip2|xz|zstd|auto-compress)' | tr '\n' ' ') 131 | 132 | echo "Starting extraction of '$archive_name'..." 133 | echo "────────────────────────────────────────────────────────" 134 | 135 | # Pipeline: Compressed (with -s) → decompress → Written (bytes out) → tar -x 136 | pv -c -N "Compressed" -s "$archive_size" "$archive_name" \ 137 | | $decompress_cmd \ 138 | | pv -c -N "Written" \ 139 | | tar -x -f - $clean_opts "${input_paths[@]}" 140 | 141 | local decompress_ec=${PIPESTATUS[1]} 142 | local tar_ec=${PIPESTATUS[3]} 143 | echo "────────────────────────────────────────────────────────" 144 | if [[ $decompress_ec -eq 0 && $tar_ec -eq 0 ]]; then 145 | echo "✅ Success: Archive extracted." 146 | else 147 | echo "❌ Error: Extraction failed." >&2 148 | exit 1 149 | fi 150 | } 151 | 152 | main "$@" 153 | -------------------------------------------------------------------------------- /tar-stats-src/create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # tar-stats-create: A 'tar' Wrapper for Archive Creation # 4 | # # 5 | # Transparently adds a progress bar during archive creation using 'pv'. # 6 | # Supports: -c -f [-v] [-z] [-j] [-J] [-Z] [-a] and dash-less variants. # 7 | # # 8 | # Based on tar-stats by Theodore R. Smith # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | ######################################################################### 11 | 12 | set -o pipefail 13 | 14 | # --- Argument Parsing & Validation --- 15 | parseargs() { 16 | archive_name="" 17 | input_paths=() 18 | passthrough_opts=() 19 | local has_file_flag=0 20 | local next_arg_is_file=0 21 | 22 | for arg in "$@"; do 23 | if [[ $next_arg_is_file -eq 1 ]]; then 24 | archive_name="$arg" 25 | next_arg_is_file=0 26 | continue 27 | fi 28 | 29 | case "$arg" in 30 | --file) has_file_flag=1; next_arg_is_file=1 ;; 31 | --file=*) has_file_flag=1; archive_name="${arg#*=}" ;; 32 | -f) has_file_flag=1; next_arg_is_file=1 ;; 33 | -*) 34 | if [[ "$arg" =~ f ]]; then has_file_flag=1; next_arg_is_file=1; fi 35 | passthrough_opts+=("$arg") 36 | ;; 37 | *) 38 | # Check if this is a dash-less tar option (e.g., "cvf", "czf") 39 | if [[ $has_file_flag -eq 0 ]] && [[ "$arg" =~ ^[a-zA-Z]+$ ]] && [[ "$arg" =~ [cf] ]]; then 40 | # This looks like dash-less tar flags 41 | if [[ "$arg" =~ f ]]; then 42 | has_file_flag=1 43 | next_arg_is_file=1 44 | fi 45 | # Add dash and pass through 46 | passthrough_opts+=("-$arg") 47 | else 48 | input_paths+=("$arg") 49 | fi 50 | ;; 51 | esac 52 | done 53 | 54 | if [[ $has_file_flag -eq 0 || -z "$archive_name" ]]; then 55 | echo "Error: The -f or --file flag is required." >&2 56 | return 1 57 | fi 58 | 59 | if [[ ${#input_paths[@]} -eq 0 ]]; then 60 | echo "Error: At least one file or directory is required." >&2 61 | return 1 62 | fi 63 | } 64 | 65 | # --- Compressor Selection --- 66 | get_compressor() { 67 | local opts="$*" 68 | local cmd="cat" 69 | local name="None" 70 | 71 | # Match compression flags only in options, not in filenames 72 | # Pattern: (start|space)-X where X is the compression letter 73 | if [[ "$opts" =~ (^|[[:space:]])--gzip([[:space:]]|$) ]] || [[ "$opts" =~ (^|[[:space:]])-[^[:space:]]*z([^[:space:]]*[[:space:]]|$) ]]; then 74 | cmd="gzip" 75 | name="Gzip" 76 | elif [[ "$opts" =~ (^|[[:space:]])--bzip2([[:space:]]|$) ]] || [[ "$opts" =~ (^|[[:space:]])-[^[:space:]]*j([^[:space:]]*[[:space:]]|$) ]]; then 77 | cmd="bzip2" 78 | name="Bzip2" 79 | elif [[ "$opts" =~ (^|[[:space:]])--xz([[:space:]]|$) ]] || [[ "$opts" =~ (^|[[:space:]])-[^[:space:]]*J([^[:space:]]*[[:space:]]|$) ]]; then 80 | cmd="xz -T0 -c" 81 | name="XZ" 82 | elif [[ "$opts" =~ (^|[[:space:]])--zstd([[:space:]]|$) ]] || [[ "$opts" =~ (^|[[:space:]])-[^[:space:]]*Z([^[:space:]]*[[:space:]]|$) ]]; then 83 | cmd="zstd" 84 | name="Zstd" 85 | elif [[ "$opts" =~ (^|[[:space:]])--auto-compress([[:space:]]|$) ]] || [[ "$opts" =~ (^|[[:space:]])-[^[:space:]]*a([^[:space:]]*[[:space:]]|$) ]]; then 86 | case "$archive_name" in 87 | *.tar.gz|*.tgz) cmd="gzip"; name="Gzip" ;; 88 | *.tar.bz2|*.tbz2) cmd="bzip2"; name="Bzip2" ;; 89 | *.tar.xz|*.txz) cmd="xz -T0 -c"; name="XZ" ;; 90 | *.tar.zst|*.tzst) cmd="zstd"; name="Zstd" ;; 91 | *) echo "Warning: Auto-detection failed for '$archive_name'." >&2 ;; 92 | esac 93 | else 94 | cmd="cat" 95 | name="None" 96 | fi 97 | 98 | echo "$cmd|$name" 99 | } 100 | 101 | # --- Main Creation Logic --- 102 | main() { 103 | parseargs "$@" || exit 1 104 | 105 | # Validate input paths exist 106 | for path in "${input_paths[@]}"; do 107 | if [[ ! -e "$path" ]]; then 108 | echo "Error: File/directory not found: '$path'" >&2 109 | exit 1 110 | fi 111 | done 112 | 113 | # Get total size 114 | echo "Calculating total size..." 115 | local total_size 116 | total_size=$(du -sbc "${input_paths[@]}" | tail -n 1 | awk '{print $1}') 117 | if ! [[ "$total_size" =~ ^[0-9]+$ ]] || [[ "$total_size" -eq 0 ]]; then 118 | echo "Error: Could not calculate archive size." >&2 119 | exit 1 120 | fi 121 | 122 | # Set up compressor 123 | local result 124 | result=$(get_compressor "$*") 125 | local compressor_cmd="${result%%|*}" 126 | local compressor_name="${result#*|}" 127 | 128 | # If no compressor, still allow no compression (e.g., .tar) 129 | if [[ "$compressor_cmd" == "cat" ]]; then 130 | echo "Creating uncompressed archive '${archive_name}'..." 131 | else 132 | echo "Creating compressed archive '${archive_name}' with ${compressor_name}..." 133 | fi 134 | 135 | echo "--------------------------------------------------------" 136 | 137 | # Build tar command with final options and pipe through pv stages 138 | tar -c -f - "${input_paths[@]}" \ 139 | | pv -c -N "Input (tar)" -s "$total_size" \ 140 | | $compressor_cmd \ 141 | | pv -c -N "Output (${compressor_name})" \ 142 | > "$archive_name" 143 | 144 | local tar_exit_code=${PIPESTATUS[0]} 145 | local compressor_exit_code=${PIPESTATUS[2]} 146 | echo "────────────────────────────────────────────────────────" 147 | if [[ $tar_exit_code -eq 0 && $compressor_exit_code -eq 0 ]]; then 148 | echo "✅ Success: Archive created." 149 | else 150 | echo "❌ Error: Archive creation failed." >&2 151 | exit 1 152 | fi 153 | } 154 | 155 | main "$@" 156 | -------------------------------------------------------------------------------- /wifi-autorun-on-connect.installer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # WiFi Autorun on Connect Installer # 4 | # # 5 | # Automatically run a script when connected to specific # 6 | # WiFi networks. # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Copyright © 2020 Theodore R. Smith # 12 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 13 | # # 14 | # License: Creative Commons Attribution v4.0 International # 15 | ######################################################################### 16 | 17 | # Require this script to be run as the root user. 18 | # @see https://serverfault.com/a/37836/56309 19 | if [[ $(/usr/bin/id -u) -ne 0 ]]; then 20 | echo "Error: You *MUST* run this as sudo or root!" 21 | exit 99 22 | fi 23 | 24 | if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then 25 | echo "Usage: wifi-autorun-on-startup.installer [HOTSPOT] [SCRIPT]" 26 | exit 27 | fi 28 | 29 | 30 | if [ ! -d "/etc/NetworkManager" ]; then 31 | echo "Error: This script only works with NetworkManager." >&2 32 | exit 2 33 | fi 34 | 35 | WIFI_HOTSPOT="" 36 | if [ ! -z "$1" ]; then 37 | if [ ! -f "/etc/NetworkManager/system-connections/${1}.nmconnection" ]; then 38 | echo "Error: NetworkManager does not know about '${1}'." >&2 39 | echo " Please try connecting to it first, retrying this script." >&2 40 | exit 3 41 | fi 42 | 43 | WIFI_HOTSPOT="$1" 44 | fi 45 | 46 | if [ ! -z "$2" ] && [ ! -f "$2" ]; then 47 | echo "Error: Cannot find a script at '${2}'." >&2 48 | exit 4 49 | fi 50 | 51 | echo "Warning: This script only works with NetworkManager..." 52 | echo "" 53 | 54 | function selectHotspot() { 55 | if [ -z "$WIFI_HOTSPOT" ]; then 56 | echo "Known Hotspots:" >&2 57 | 58 | HOTSPOTS=() 59 | local index=0 60 | local file 61 | for file in /etc/NetworkManager/system-connections/*.nmconnection; do 62 | index=$(( $index + 1 )) 63 | filename=${file##*/system-connections/} 64 | hotspot=${filename%%.nmconnection} 65 | HOTSPOTS+=("$hotspot") 66 | 67 | echo " ${index}) ${hotspot}" >&2 68 | done 69 | 70 | echo "" >&2 71 | echo -n "Which hotspot (1-${index})? " >&2 72 | read hotspot_pick 73 | hotspot_pick=$(( $hotspot_pick - 1 )) 74 | 75 | echo "${HOTSPOTS[$hotspot_pick]}" 76 | else 77 | echo $WIFI_HOTSPOT 78 | fi 79 | } 80 | 81 | WIFI_HOTSPOT=$(selectHotspot) 82 | 83 | echo "Chosen wifi network: ${WIFI_HOTSPOT}" 84 | 85 | function assertScriptDoesNotExit() 86 | { 87 | SCRIPT_FILE="/etc/NetworkManager/dispatcher.d/on_connect-${1}.sh" 88 | if [ -f "$SCRIPT_FILE" ]; then 89 | echo "WARNING: A startup script for this connection already exists" >&2 90 | echo " at '${SCRIPT_FILE}'." >&2 91 | echo "" >&2 92 | echo -n "Replace it or quit? [r/Q] " >&2 93 | 94 | local replace_or_quit 95 | read replace_or_quit 96 | if [ "$replace_or_quit" != "r" ]; then 97 | echo "Quitting..." >&2 98 | exit 5 99 | fi 100 | fi 101 | } 102 | 103 | assertScriptDoesNotExit "${WIFI_HOTSPOT}" 104 | 105 | function grabConnectionUUID() 106 | { 107 | local nm_conn_file="/etc/NetworkManager/system-connections/${1}.nmconnection" 108 | 109 | if [ ! -f "$nm_conn_file" ]; then 110 | echo "Error: NetworkManager does not know about '${1}'." >&2 111 | echo " Please try connecting to it first, retrying this script." >&2 112 | exit 3 113 | fi 114 | 115 | CONNECTION_UUID=$(grep uuid= "$nm_conn_file" | sed 's/^uuid=//') 116 | echo "Connection UUID: $CONNECTION_UUID" 117 | } 118 | 119 | grabConnectionUUID "$WIFI_HOTSPOT" 120 | 121 | 122 | ############################################################# 123 | # Grab the text from an existing file -or- user input... # 124 | # # 125 | # Copyright © 2020 Theodore R. Smith # 126 | # License: Creative Commons Attribution v4.0 International # 127 | # From: https://github.com/hopeseekr/BashScripts/ # 128 | # @see https://stackoverflow.com/a/64486155/430062 # 129 | ############################################################# 130 | function grabDocument() 131 | { 132 | if [ ! -z "$1" ] && [ -f "$1" ]; then 133 | echo $(<"$1") 134 | else 135 | echo "" >&2 136 | echo "Please type/paste in bash script you wish to be run when NetworkManager connects to '${WIFI_HOTSPOT}'." >&2 137 | echo "You should NOT start with '#!/usr/bin/env bash'..." >&2 138 | echo "Press CTRL+D when finished." >&2 139 | echo "" >&2 140 | 141 | # Read user input until CTRL+D. 142 | # @see https://stackoverflow.com/a/38811806/430062 143 | readarray -t user_input 144 | 145 | # Output as a newline-dilemeted string. 146 | # @see https://stackoverflow.com/a/15692004/430062 147 | printf '%s\n' "${user_input[@]}" 148 | fi 149 | } 150 | 151 | N=$'\n' 152 | SCRIPT="#!/usr/bin/env bash${N}" 153 | SCRIPT+="if [[ CONNECTION_UUID=\"${CONNECTION_UUID}\" ]]; then${N}" 154 | SCRIPT+="$(grabDocument "$2")${N}" 155 | SCRIPT+="fi${N}" 156 | 157 | # Preserve white spaces and newlines. 158 | # @see https://stackoverflow.com/a/18018422/430062 159 | echo "$SCRIPT" > "$SCRIPT_FILE" 160 | 161 | chmod 0755 "$SCRIPT_FILE" 162 | 163 | echo "" 164 | echo "Success! An autorun on connect script has been successfully installed for ${WIFI_HOTSPOT}." 165 | -------------------------------------------------------------------------------- /git-filter-copy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################# 3 | # git-filter-copy: Copies Git repositories while filtering untracked files # 4 | # # 5 | # Usage: # 6 | # git-filter-copy # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # This command creates a clean copy of a Git repository (or regular directory) # 12 | # by including only committed files and modified tracked files while respecting # 13 | # .gitattributes rules (e.g., export-ignore). Untracked files and directories # 14 | # are excluded from the copy. For non-Git directories, performs a full copy. # 15 | # # 16 | # Copyright © 2025 Theodore R. Smith # 17 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 18 | # # 19 | # License: Creative Commons Attribution v4.0 International # 20 | ################################################################################# 21 | 22 | # Exit immediately if a command exits with a non-zero status. 23 | set -e 24 | 25 | # --- Function Definitions --- 26 | 27 | # Prints usage information and exits. 28 | usage() { 29 | echo "Usage: $0 " 30 | exit 1 31 | } 32 | 33 | # Resolves a potentially relative path to an absolute path. 34 | resolve_absolute_path() { 35 | local path_to_resolve="$1" 36 | local current_dir="$2" 37 | 38 | # Use a more robust method to get the absolute path 39 | if [ -d "$path_to_resolve" ]; then 40 | (cd "$path_to_resolve" && pwd) 41 | else 42 | # Handle case where the last component might not exist yet 43 | local parent_dir 44 | parent_dir=$(dirname "$path_to_resolve") 45 | local base_name 46 | base_name=$(basename "$path_to_resolve") 47 | echo "$(cd "$parent_dir" && pwd)/$base_name" 48 | fi 49 | } 50 | 51 | # Handles the simple case of a non-Git directory by copying it entirely. 52 | handle_non_git_repo() { 53 | local src="$1" 54 | local dest="$2" 55 | echo "Source is not a Git repository. Copying entire directory..." 56 | # Ensure destination parent directory exists 57 | mkdir -p "$(dirname "$dest")" 58 | cp -a "$src" "$dest" 59 | } 60 | 61 | # Exports a clean archive and then overwrites it with any local modifications 62 | # to tracked files, while respecting .gitattributes `export-ignore`. 63 | export_git_repo() { 64 | local src_dir="$1" 65 | local dest_dir="$2" 66 | local project_name 67 | 68 | project_name=$(basename "$src_dir") 69 | local final_dest="$dest_dir/$project_name" 70 | 71 | echo "Exporting Git repository from '$src_dir' to '$final_dest'..." 72 | 73 | # Ensure the final destination directory exists and is empty 74 | rm -rf "$final_dest" 75 | mkdir -p "$final_dest" 76 | 77 | # Use a subshell to isolate the 'cd' command. 78 | ( 79 | cd "$src_dir" || { echo "ERROR: Cannot change to directory: $src_dir"; exit 1; } 80 | 81 | # --- Step 1: Create a clean archive from HEAD --- 82 | # This correctly handles all .gitattributes `export-ignore` rules for the base export. 83 | echo "Step 1: Creating base archive from HEAD..." 84 | git archive HEAD | (cd "$final_dest" && tar -xf -) 85 | 86 | # --- Step 2: Find MODIFIED files and copy them over --- 87 | # This overwrites files in the destination with your uncommitted changes. 88 | echo "Step 2: Overwriting with modified tracked files..." 89 | 90 | # `git diff --name-only HEAD` is a reliable way to get modified files. 91 | # We loop through them to handle filenames with spaces correctly. 92 | # The trailing '--' ensures paths are not mistaken for options. 93 | git diff --name-only HEAD -- | while IFS= read -r file; do 94 | # We must still check if this modified file should be exported. 95 | # This prevents copying a modified file from a directory that was 96 | # correctly excluded by `git archive` (e.g., /tests). 97 | local attr_output 98 | attr_output=$(git check-attr export-ignore -- "$file" 2>/dev/null || true) 99 | 100 | if ! echo "$attr_output" | grep -q "export-ignore: set"; then 101 | # This file is modified and is NOT export-ignored. 102 | # Only copy it if it actually exists (it could have been deleted). 103 | if [ -f "$file" ]; then 104 | echo " - Copying modified file: $file" 105 | # We don't need to create the directory, as `git archive` 106 | # would have already created it for the original file. 107 | cp -a "$file" "$final_dest/$file" 108 | fi 109 | fi 110 | done 111 | ) 112 | echo "Export complete." 113 | } 114 | 115 | # --- Main Script Logic --- 116 | 117 | main() { 118 | # 1. Validate Arguments 119 | if [ $# -ne 2 ]; then 120 | usage 121 | fi 122 | 123 | local src_dir="$1" 124 | local dest_dir="$2" 125 | local CWD 126 | CWD="$PWD" 127 | 128 | # 2. Prepare Variables 129 | src_dir=$(resolve_absolute_path "$src_dir" "$CWD") 130 | dest_dir=$(resolve_absolute_path "$dest_dir" "$CWD") 131 | 132 | # 3. Decide Action: Git Repo or Regular Directory? 133 | if [ ! -d "$src_dir/.git" ]; then 134 | # For a non-git repo, we copy to a subdir named after the source 135 | handle_non_git_repo "$src_dir" "$dest_dir/$(basename "$src_dir")" 136 | else 137 | export_git_repo "$src_dir" "$dest_dir" 138 | fi 139 | } 140 | 141 | # Run the main function, passing all command-line arguments to it 142 | main "$@" 143 | -------------------------------------------------------------------------------- /git-change-author: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### ==== git-change-author ==== 3 | ########################################################################## 4 | # git-change-author: Git Bulk Commit Author Rewriter # 5 | # # 6 | # Easily change the name and email of any of the commits in # 7 | # a git repository, while preserving original commit times and # 8 | # (re)signing commits with the new author's key (if configured). # 9 | # # 10 | # ./git-change-author "Your Name" "email@address" [SHA1] # 11 | # # 12 | # SHA1: The SHA1 of the *first* commit to change. If omitted, # 13 | # all commits in the history will be changed. # 14 | # # 15 | # Part of HopeSeekr's BashScripts Collection # 16 | # https://github.com/hopeseekr/BashScripts/ # 17 | # # 18 | # Copyright © 2012-2021 Theodore R. Smith # 19 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 20 | # # 21 | # License: Creative Commons Attribution v4.0 International # 22 | # # 23 | ########################################################################## 24 | 25 | if [[ -z "$1" || "$1" == "--help" || -z "$2" ]]; then 26 | echo ' Usage: git-change-author "Your Name" "email@address" [SHA1]' 27 | echo ' SHA1: The SHA1 of the *first* commit to change. If omitted, all commits are changed.' 28 | exit 1 29 | fi 30 | 31 | AUTHOR="$1" 32 | EMAIL="$2" 33 | START_HASH_PARAM="${3}" # This is the first commit the user wants to modify (inclusive) 34 | 35 | # --- Start of git-same-sig-time logic integration --- 36 | 37 | # Verify faketime is installed 38 | if ! command -v faketime &> /dev/null 39 | then 40 | echo "'faketime' could not be found. Please install it first." 41 | echo " Ubuntu/Debian: sudo apt-get install faketime" 42 | echo " macOS: brew install libfaketime" 43 | exit 1 44 | fi 45 | 46 | # Determine the base for rebase and commit count 47 | # START_HASH_ACTUAL will hold the validated SHA1 of the first commit to process, or be empty if all commits. 48 | START_HASH_ACTUAL="" 49 | if [[ -n "${START_HASH_PARAM}" ]]; then 50 | # Validate START_HASH_PARAM is a valid commit 51 | if ! git cat-file -t "${START_HASH_PARAM}" &> /dev/null || [[ "$(git cat-file -t "${START_HASH_PARAM}")" != "commit" ]]; then 52 | echo "Error: '${START_HASH_PARAM}' is not a valid commit SHA1." 53 | exit 1 54 | fi 55 | START_HASH_ACTUAL="${START_HASH_PARAM}" 56 | fi 57 | 58 | # Determine if we are starting from the root commit or a specific commit. 59 | # The root commit has no parents. 60 | ROOT_COMMIT_SHA=$(git rev-list --max-parents=0 HEAD) 61 | 62 | if [[ -z "${START_HASH_ACTUAL}" || "${START_HASH_ACTUAL}" == "${ROOT_COMMIT_SHA}" ]]; then 63 | # Case 1: No SHA1 provided, or provided SHA1 is the root commit. Process all commits. 64 | REBASE_BASE="--root" 65 | COMMIT_COUNT=$(git rev-list --count HEAD) 66 | echo "Changing author for all $COMMIT_COUNT commits from the beginning of history." 67 | else 68 | # Case 2: Specific SHA1 provided (which is not the root). Process from this SHA1 onwards. 69 | REBASE_BASE="${START_HASH_ACTUAL}^" 70 | # Count commits from START_HASH_ACTUAL (inclusive) to HEAD. 71 | # git log SHA1^..HEAD counts commits reachable from HEAD but not SHA1^ (i.e., SHA1 and its descendants). 72 | COMMIT_COUNT=$(git log --oneline "${START_HASH_ACTUAL}^..HEAD" | wc -l) 73 | echo "Changing author for $COMMIT_COUNT commits starting from ${START_HASH_ACTUAL} up to HEAD." 74 | fi 75 | 76 | if [ "$COMMIT_COUNT" -eq 0 ]; then 77 | echo "No commits found to process. Exiting." 78 | exit 0 79 | fi 80 | 81 | echo "" 82 | echo "Initiating interactive rebase to change commit authors, preserve original commit times, and re-sign commits." 83 | echo "All relevant commits ($COMMIT_COUNT) will automatically be marked for 'edit'." 84 | echo "" 85 | 86 | # Use GIT_SEQUENCE_EDITOR to automatically change 'pick' to 'edit' for all commits. 87 | # --rebase-merges is crucial for preserving merge commits properly. 88 | GIT_SEQUENCE_EDITOR="sed -i -re 's/^pick/edit/'" git rebase --rebase-merges -i ${REBASE_BASE} 89 | 90 | # The rebase loop. It will iterate for each commit marked 'edit'. 91 | for i in $(eval echo {1.."$COMMIT_COUNT"}); do 92 | echo "Processing commit $i of $COMMIT_COUNT..." 93 | 94 | # Get the original commit date/time and timezone. 95 | # This format is compatible with faketime: "YYYY-MM-DD HH:MM:SS +ZZZZ" 96 | COMMIT_DATETIME_ZONE=$(git log -1 --pretty=format:%ad --date=format:"%Y-%m-%d %H:%M:%S %z" HEAD) 97 | 98 | # Use faketime to set the current time to the original commit time. 99 | # Then, amend the commit with the new author details, re-sign it (-S), 100 | # and keep the original commit message (--no-edit). 101 | faketime "${COMMIT_DATETIME_ZONE}" git \ 102 | -c user.name="${AUTHOR}" \ 103 | -c user.email="${EMAIL}" \ 104 | commit --amend -S --no-edit --author "${AUTHOR} <${EMAIL}>" 105 | 106 | if [ "$?" -ne 0 ]; then 107 | echo "Error amending commit $i. Aborting rebase." 108 | git rebase --abort 109 | exit 1 110 | fi 111 | 112 | # Continue the rebase to the next commit. 113 | git rebase --continue 114 | if [ "$?" -ne 0 ]; then 115 | # This can happen if it's the last commit and rebase finishes successfully, 116 | # or if there's an actual conflict/issue during continue. 117 | # Break the loop in case rebase is done or encountered an unhandled error. 118 | echo "Rebase continue command returned non-zero. Assuming rebase is complete or encountered an unhandled error." 119 | break 120 | fi 121 | done 122 | 123 | echo "" 124 | echo "" 125 | echo "Git rebase completed. All processed commits now have the new author, preserved original dates," 126 | echo "and have been re-signed with the new author's GPG key (if configured)." 127 | echo "Don't forget to run \`git push -f\`... preferably on a test branch first!" 128 | 129 | -------------------------------------------------------------------------------- /git-shift-time: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ############################################################################### 3 | # git-shift-time: Shift or set commit timestamps from a target commit to HEAD # 4 | # # 5 | # Usage: # 6 | # git-shift-time <±minutes> [new-date] # 7 | # # 8 | # Part of HopeSeekr's BashScripts Collection # 9 | # https://github.com/hopeseekr/BashScripts/ # 10 | # # 11 | # Examples: # 12 | # git-shift-time abc123 -60 # shift each by -1 hour # 13 | # git-shift-time abc123 +120 "2024-12-31 23:59:59" # set exact date # 14 | # # 15 | # WARNING: This rewrites history. Do NOT use on shared branches unless you # 16 | # know what you're doing. After success, push with: # 17 | # git push --force-with-lease # 18 | # # 19 | # Copyright © 2024 Theodore R. Smith # 20 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 21 | # # 22 | # License: Creative Commons Attribution v4.0 International # 23 | ############################################################################### 24 | 25 | set -euo pipefail 26 | 27 | # Verify faketime is installed 28 | if ! command -v faketime &> /dev/null 29 | then 30 | echo "'faketime' could not be found. Please install it first." 31 | echo " Ubuntu/Debian: sudo apt-get install faketime" 32 | echo " macOS: brew install libfaketime" 33 | exit 1 34 | fi 35 | 36 | # Function to display usage 37 | usage() { 38 | echo "Usage: $0 [target-date]" 39 | echo "" 40 | echo "Arguments:" 41 | echo " commit-hash : The commit hash to start from (inclusive)" 42 | echo " minutes-offset : Number of minutes to adjust (positive or negative)" 43 | echo " target-date : Optional specific date/time to set (overrides minutes-offset)" 44 | echo " Format: 'YYYY-MM-DD HH:MM:SS' or any date format git accepts" 45 | echo "" 46 | echo "Examples:" 47 | echo " $0 abc123 -30 # Subtract 30 minutes from commit time" 48 | echo " $0 abc123 60 # Add 60 minutes to commit time" 49 | echo " $0 abc123 0 '2024-01-15 14:30:00' # Set to specific date/time" 50 | exit 1 51 | } 52 | 53 | # Check for required arguments 54 | if [ $# -lt 2 ]; then 55 | usage 56 | fi 57 | 58 | COMMIT_HASH="$1" 59 | MINUTES_OFFSET="$2" 60 | TARGET_DATE="${3:-}" 61 | 62 | # Verify git pretty is configured 63 | git pretty -1 > /dev/null 2>&1 64 | if [ "$?" -eq 1 ]; then 65 | echo "Error: You need \`git pretty\` installed. Add this to your ~/.gitconfig:" 66 | echo "" 67 | echo " [pretty]" 68 | echo " shortlog = format:%C(auto,yellow)%h%C(auto,magenta)% G? %Cred%ad %C(auto,green)%<(20,trunc)%aN%C(auto,reset)%s%C(auto,red)% gD% D" 69 | exit 1 70 | fi 71 | 72 | # Validate minutes offset is a number (positive or negative) 73 | if [[ ! "$MINUTES_OFFSET" =~ ^-?[0-9]+$ ]] && [ -z "$TARGET_DATE" ]; then 74 | echo "Error: Minutes offset must be an integer (positive or negative)" 75 | usage 76 | fi 77 | 78 | # Verify the commit exists 79 | if ! git rev-parse "$COMMIT_HASH" > /dev/null 2>&1; then 80 | echo "Error: Commit $COMMIT_HASH not found" 81 | exit 1 82 | fi 83 | 84 | # Get the count of git commits including and after the target commit 85 | COMMIT_COUNT=$(git log --oneline "$COMMIT_HASH"^.. 2>/dev/null | wc -l) 86 | 87 | if [ "$COMMIT_COUNT" -eq 0 ]; then 88 | echo "Error: No commits found after $COMMIT_HASH" 89 | exit 1 90 | fi 91 | 92 | echo "Found $COMMIT_COUNT commit(s) to adjust" 93 | echo "Starting time adjustment..." 94 | 95 | # Start interactive rebase 96 | # Test that it isn't the root node. 97 | if git rev-parse --quiet --verify "$COMMIT_HASH"^ >/dev/null; then 98 | GIT_SEQUENCE_EDITOR="sed -i -re 's/^pick/edit/'" git rebase -i "$COMMIT_HASH"^ 99 | else 100 | GIT_SEQUENCE_EDITOR="sed -i -re 's/^pick/edit/'" git rebase -i --root 101 | fi 102 | 103 | # Process each commit 104 | for i in $(seq 1 "$COMMIT_COUNT"); do 105 | # Get current commit hash for display 106 | CURRENT_HASH=$(git rev-parse --short HEAD) 107 | 108 | if [ -n "$TARGET_DATE" ]; then 109 | # Use the specified target date 110 | NEW_TIME="$TARGET_DATE" 111 | echo "[$i/$COMMIT_COUNT] Setting commit $CURRENT_HASH to: $NEW_TIME" 112 | else 113 | # Calculate new time based on offset 114 | # Get current commit timestamp in seconds since epoch 115 | CURRENT_TIMESTAMP=$(git log -1 --format=%at) 116 | 117 | # Calculate new timestamp (minutes * 60 seconds) 118 | NEW_TIMESTAMP=$((CURRENT_TIMESTAMP + (MINUTES_OFFSET * 60))) 119 | 120 | # Convert back to readable format for display 121 | if [[ "$OSTYPE" == "darwin"* ]]; then 122 | # macOS 123 | NEW_TIME=$(date -r "$NEW_TIMESTAMP" "+%Y-%m-%d %H:%M:%S") 124 | else 125 | # Linux 126 | NEW_TIME=$(date -d "@$NEW_TIMESTAMP" "+%Y-%m-%d %H:%M:%S") 127 | fi 128 | 129 | echo "[$i/$COMMIT_COUNT] Adjusting commit $CURRENT_HASH by $MINUTES_OFFSET minutes to: $NEW_TIME" 130 | fi 131 | 132 | # Use faketime to set the commit time 133 | # We need to preserve the author date and set both author and committer dates 134 | if [ -n "$TARGET_DATE" ]; then 135 | # For specific date, use it directly 136 | GIT_AUTHOR_DATE="$TARGET_DATE" \ 137 | GIT_COMMITTER_DATE="$TARGET_DATE" \ 138 | faketime "$TARGET_DATE" git commit --amend -S --no-edit --date="$TARGET_DATE" 139 | else 140 | # For offset, use the calculated time 141 | GIT_AUTHOR_DATE="$NEW_TIME" \ 142 | GIT_COMMITTER_DATE="$NEW_TIME" \ 143 | faketime "$NEW_TIME" git commit --amend -S --no-edit --date="$NEW_TIME" 144 | fi 145 | 146 | # Continue to next commit 147 | git rebase --continue 148 | done 149 | 150 | echo "" 151 | echo "✓ Successfully adjusted $COMMIT_COUNT commit(s)" 152 | echo "" 153 | echo "Updated commits:" 154 | git pretty -"$COMMIT_COUNT" 155 | -------------------------------------------------------------------------------- /tar-stats-src/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # build.sh: Build the tar-stats unified script # 4 | # # 5 | # Combines create.sh, extract.sh, and help.sh into a single # 6 | # tar-stats executable with dash-optional support. # 7 | ######################################################################### 8 | 9 | set -e 10 | 11 | OUTPUT_FILE="tar-stats" 12 | BUILD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 13 | 14 | echo "Building tar-stats from source files..." 15 | 16 | # Verify source files exist 17 | if [[ ! -f "$BUILD_DIR/create.sh" ]]; then 18 | echo "Error: create.sh not found!" >&2 19 | exit 1 20 | fi 21 | 22 | if [[ ! -f "$BUILD_DIR/extract.sh" ]]; then 23 | echo "Error: extract.sh not found!" >&2 24 | exit 1 25 | fi 26 | 27 | if [[ ! -f "$BUILD_DIR/help.sh" ]]; then 28 | echo "Error: help.sh not found!" >&2 29 | exit 1 30 | fi 31 | 32 | # Extract the header from help.sh (everything up to show_help function) 33 | extract_header() { 34 | sed -n '1,/^show_help()/p' "$BUILD_DIR/help.sh" | sed '$d' 35 | } 36 | 37 | # Extract show_help function from help.sh 38 | extract_help_function() { 39 | sed -n '/^show_help()/,/^}$/p' "$BUILD_DIR/help.sh" 40 | } 41 | 42 | # Extract functions from create.sh (skip shebang, header comments, and main function) 43 | extract_create_functions() { 44 | sed -n '/^# --- Help\/Usage Function ---/,/^main() {/p' "$BUILD_DIR/create.sh" | sed '$d' 45 | } 46 | 47 | # Extract functions from extract.sh (skip shebang, header comments, and main function) 48 | extract_extract_functions() { 49 | sed -n '/^# --- Help\/Usage Function ---/,/^main() {/p' "$BUILD_DIR/extract.sh" | sed '$d' 50 | } 51 | 52 | # Start building the output file 53 | cat > "$OUTPUT_FILE" << 'HEADER_END' 54 | HEADER_END 55 | 56 | # Add header from help.sh 57 | extract_header >> "$OUTPUT_FILE" 58 | 59 | # Add blank line 60 | echo "" >> "$OUTPUT_FILE" 61 | 62 | # Add main show_help function 63 | extract_help_function >> "$OUTPUT_FILE" 64 | 65 | echo "" >> "$OUTPUT_FILE" 66 | 67 | # Add all functions from create.sh (excluding help and main) 68 | echo "# --- CREATE MODE FUNCTIONS ---" >> "$OUTPUT_FILE" 69 | sed -n '/^parseargs()/,/^main() {/p' "$BUILD_DIR/create.sh" | sed '$d' >> "$OUTPUT_FILE" 70 | 71 | echo "" >> "$OUTPUT_FILE" 72 | 73 | # Add all functions from extract.sh (excluding help and main) 74 | echo "# --- EXTRACT MODE FUNCTIONS ---" >> "$OUTPUT_FILE" 75 | sed -n '/^# --- Decompressor Selection ---/,/^main() {/p' "$BUILD_DIR/extract.sh" | sed '$d' >> "$OUTPUT_FILE" 76 | 77 | echo "" >> "$OUTPUT_FILE" 78 | 79 | # Add the unified main function 80 | cat >> "$OUTPUT_FILE" << 'MAIN_FUNCTION' 81 | # --- CREATE MODE WRAPPER --- 82 | mode_create() { 83 | parseargs "$@" || exit 1 84 | 85 | # Validate input paths exist 86 | for path in "${input_paths[@]}"; do 87 | if [[ ! -e "$path" ]]; then 88 | echo "Error: File/directory not found: '$path'" >&2 89 | exit 1 90 | fi 91 | done 92 | 93 | # Get total size 94 | echo "Calculating total size..." 95 | local total_size 96 | total_size=$(du -sbc "${input_paths[@]}" | tail -n 1 | awk '{print $1}') 97 | if ! [[ "$total_size" =~ ^[0-9]+$ ]] || [[ "$total_size" -eq 0 ]]; then 98 | echo "Error: Could not calculate archive size." >&2 99 | exit 1 100 | fi 101 | 102 | # Set up compressor 103 | local result 104 | result=$(get_compressor "$*") 105 | local compressor_cmd="${result%%|*}" 106 | local compressor_name="${result#*|}" 107 | 108 | if [[ "$compressor_cmd" == "cat" ]]; then 109 | echo "Creating uncompressed archive '${archive_name}'..." 110 | else 111 | echo "Creating compressed archive '${archive_name}' with ${compressor_name}..." 112 | fi 113 | 114 | echo "--------------------------------------------------------" 115 | 116 | # Build tar command with progress 117 | tar -c -f - "${input_paths[@]}" \ 118 | | pv -c -N "Input (tar)" -s "$total_size" \ 119 | | $compressor_cmd \ 120 | | pv -c -N "Output (${compressor_name})" \ 121 | > "$archive_name" 122 | 123 | local exit_code=${PIPESTATUS[2]} 124 | echo "────────────────────────────────────────────────────────" 125 | if [[ $exit_code -eq 0 ]]; then 126 | echo "✅ Success: Archive created." 127 | else 128 | echo "❌ Error: Archive creation failed." >&2 129 | exit 1 130 | fi 131 | } 132 | 133 | # --- EXTRACT MODE WRAPPER --- 134 | mode_extract() { 135 | parseargs "$@" || exit 1 136 | 137 | # Validate archive exists 138 | if [[ ! -f "$archive_name" ]]; then 139 | echo "Error: Archive not found: '$archive_name'" >&2 140 | exit 1 141 | fi 142 | 143 | # Get archive size 144 | local archive_size 145 | archive_size=$(stat -c %s "${archive_name}" 2>/dev/null || stat -f %z "${archive_name}" 2>/dev/null) 146 | if ! [[ "$archive_size" =~ ^[0-9]+$ ]] || [[ "$archive_size" -eq 0 ]]; then 147 | echo "Error: Could not determine archive size." >&2 148 | exit 1 149 | fi 150 | 151 | # Set decompressor 152 | local decompress_cmd 153 | decompress_cmd=$(get_decompressor "$archive_name") 154 | 155 | # Cleanup options: strip compression flags 156 | local clean_opts 157 | clean_opts=$(echo "$*" | tr ' ' '\n' | grep -vE -- '-[zjJZa]|--(?:gzip|bzip2|xz|zstd|auto-compress)' | tr '\n' ' ') 158 | 159 | echo "Starting extraction of '$archive_name'..." 160 | echo "────────────────────────────────────────────────────────" 161 | 162 | # Pipeline: Compressed → decompress → Written → tar -x 163 | pv -c -N "Compressed" -s "$archive_size" "$archive_name" \ 164 | | $decompress_cmd \ 165 | | pv -c -N "Written" \ 166 | | tar -x -f - $clean_opts "${input_paths[@]}" 167 | 168 | local exit_code=${PIPESTATUS[3]} 169 | echo "────────────────────────────────────────────────────────" 170 | if [[ $exit_code -eq 0 ]]; then 171 | echo "✅ Success: Archive extracted." 172 | else 173 | echo "❌ Error: Extraction failed." >&2 174 | exit 1 175 | fi 176 | } 177 | 178 | # --- MAIN ENTRY POINT --- 179 | main() { 180 | if [[ $# -eq 0 ]] || [[ " $* " =~ (--help|-h) ]]; then 181 | show_help 182 | exit 0 183 | fi 184 | 185 | # Detect mode (create or extract) 186 | # Support both dashed (-cvf, -xvf) and dash-less (cvf, xvf) formats 187 | local mode="" 188 | for arg in "$@"; do 189 | case "$arg" in 190 | -c|--create) mode="create"; break ;; 191 | -x|--extract) mode="extract"; break ;; 192 | -*c*) mode="create"; break ;; 193 | -*x*) mode="extract"; break ;; 194 | *) 195 | # Check for dash-less format (e.g., cvf, xvf) 196 | if [[ "$arg" =~ ^[a-zA-Z]+$ ]]; then 197 | if [[ "$arg" =~ c ]]; then 198 | mode="create" 199 | break 200 | elif [[ "$arg" =~ x ]]; then 201 | mode="extract" 202 | break 203 | fi 204 | fi 205 | ;; 206 | esac 207 | done 208 | 209 | if [[ -z "$mode" ]]; then 210 | echo "Error: Must specify either -c (create) or -x (extract) mode." >&2 211 | echo "Use --help for usage information." >&2 212 | exit 1 213 | fi 214 | 215 | # Dispatch to appropriate mode 216 | if [[ "$mode" == "create" ]]; then 217 | mode_create "$@" 218 | else 219 | mode_extract "$@" 220 | fi 221 | } 222 | 223 | main "$@" 224 | MAIN_FUNCTION 225 | 226 | # Make executable 227 | chmod +x "$OUTPUT_FILE" 228 | 229 | echo "✅ Build complete: $OUTPUT_FILE" 230 | echo " Source files compiled:" 231 | echo " - create.sh" 232 | echo " - extract.sh" 233 | echo " - help.sh" 234 | echo "" 235 | echo " Dash-optional support enabled:" 236 | echo " - tar-stats -cvf archive.tar.gz dir/" 237 | echo " - tar-stats cvf archive.tar.gz dir/" 238 | echo " - tar-stats -xvf archive.tar.gz" 239 | echo " - tar-stats xvf archive.tar.gz" 240 | -------------------------------------------------------------------------------- /obs-global-hotkeys: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ############################################################################ 3 | # OBS Global Hotkeys Installer (GNOME Wayland + Xorg) # 4 | # # 5 | Quickly installs true OBS Global Hotkeys in GNOME Wayland + Xorg. This script is idempotent and safely finds available keybinding slots without overwriting existing user configurations. # 6 | # # 7 | # Part of HopeSeekr's BashScripts Collection # 8 | # https://github.com/hopeseekr/BashScripts/ # 9 | # # 10 | # License: CC BY 4.0 # 11 | ############################################################################ 12 | 13 | set -euo pipefail 14 | 15 | # ---- Configuration ------------------------------------------------------------ 16 | # Add or remove keybindings here. Ensure all three arrays have the same 17 | # number of elements. 18 | 19 | declare -a KEYBINDING_NAMES=( 20 | "OBS Record/Stop" 21 | "OBS Pause/Unpause" 22 | ) 23 | declare -a KEYBINDING_BINDS=( 24 | "F8" 25 | "F9" 26 | ) 27 | declare -a KEYBINDING_CMDS=( 28 | "obs-cmd recording toggle" 29 | "obs-cmd recording toggle-pause" 30 | ) 31 | 32 | # ---- DConf Constants ---------------------------------------------------------- 33 | readonly GSETTINGS_SCHEMA="org.gnome.settings-daemon.plugins.media-keys" 34 | readonly KB_LIST_KEY="custom-keybindings" 35 | readonly KB_ROOT_PATH="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings" 36 | readonly KB_SCHEMA_PREFIX="org.gnome.settings-daemon.plugins.media-keys.custom-keybinding" 37 | 38 | # ---- UI/Helpers --------------------------------------------------------------- 39 | # Color codes for output 40 | readonly C_RED='\e[31m' 41 | readonly C_GREEN='\e[32m' 42 | readonly C_YELLOW='\e[33m' 43 | readonly C_BLUE='\e[34m' 44 | readonly C_NC='\e[0m' # No Color 45 | 46 | info() { echo -e "${C_BLUE}==>${C_NC} $1"; } 47 | success() { echo -e "${C_GREEN}==>${C_NC} $1"; } 48 | warn() { echo -e "${C_YELLOW}==>${C_NC} $1"; } 49 | error() { >&2 echo -e "${C_RED}==> ERROR:${C_NC} $1"; } 50 | 51 | require() { 52 | command -v "$1" >/dev/null 2>&1 || { error "$1 is not installed. Please install it to continue."; exit 1; } 53 | } 54 | 55 | # Safely gets a string array from dconf and returns it as a Bash array 56 | get_dconf_array() { 57 | local gsettings_output 58 | gsettings_output=$(gsettings get "$GSETTINGS_SCHEMA" "$KB_LIST_KEY") 59 | 60 | # Clean up the string "[ 'val1', 'val2' ]" into a format that can be read into a bash array 61 | # Handles both "@as []" and "[]" for empty arrays 62 | if [[ "$gsettings_output" == "@as []" || "$gsettings_output" == "[]" ]]; then 63 | echo "" 64 | return 65 | fi 66 | # Remove brackets, quotes, and commas, then feed to `mapfile` 67 | gsettings_output="${gsettings_output//\'/}" 68 | gsettings_output="${gsettings_output//, / }" 69 | gsettings_output="${gsettings_output#[}" 70 | gsettings_output="${gsettings_output%]}" 71 | echo "$gsettings_output" 72 | } 73 | 74 | # Safely writes a Bash array to a dconf string array 75 | set_dconf_array() { 76 | local -n arr="$1" # Use nameref for passing array 77 | local formatted_array 78 | 79 | if [ ${#arr[@]} -eq 0 ]; then 80 | formatted_array="[]" 81 | else 82 | # Format as ['/path/1/', '/path/2/'] 83 | formatted_array=$(printf "'%s'," "${arr[@]}") 84 | formatted_array="[${formatted_array%,}]" # Add brackets and remove trailing comma 85 | fi 86 | 87 | gsettings set "$GSETTINGS_SCHEMA" "$KB_LIST_KEY" "$formatted_array" 88 | } 89 | 90 | usage() { 91 | cat << EOF 92 | Usage: $0 {install|uninstall|--help|-h} 93 | 94 | Installs or uninstalls global hotkeys for OBS Studio in GNOME. 95 | 96 | Commands: 97 | install Installs the keybindings, finding available slots automatically. 98 | uninstall Removes keybindings created by this script by name. 99 | --help, -h Show this help message. 100 | EOF 101 | exit 2 102 | } 103 | 104 | # ---- Main Logic --------------------------------------------------------------- 105 | 106 | install_keybindings() { 107 | info "Installing OBS global hotkeys for GNOME..." 108 | 109 | # Read current keybindings into a bash array 110 | mapfile -t current_bindings < <(get_dconf_array) 111 | 112 | # Get a list of names of all currently installed custom keybindings 113 | declare -A existing_names 114 | for path in "${current_bindings[@]}"; do 115 | # Skip empty paths that might result from parsing 116 | [[ -z "$path" ]] && continue 117 | local name 118 | # Use || echo "" to prevent errors on unset keys 119 | name=$(gsettings get "${KB_SCHEMA_PREFIX}:${path}" name 2>/dev/null || echo "") 120 | if [[ -n "$name" ]]; then 121 | existing_names["$name"]="$path" 122 | fi 123 | done 124 | 125 | local bindings_changed=false 126 | for i in "${!KEYBINDING_NAMES[@]}"; do 127 | local name="${KEYBINDING_NAMES[$i]}" 128 | local bind="${KEYBINDING_BINDS[$i]}" 129 | local cmd="${KEYBINDING_CMDS[$i]}" 130 | 131 | if [[ -n "${existing_names[$name]+_}" ]]; then 132 | warn "Keybinding '$name' already exists. Skipping." 133 | # Optional: could update the existing one here if desired 134 | continue 135 | fi 136 | 137 | # Find the next available custom keybinding slot 138 | local free_slot_path="" 139 | for j in {0..99}; do 140 | local potential_path="${KB_ROOT_PATH}/custom${j}/" 141 | # Check if this path is in our list of current bindings 142 | if ! printf '%s\n' "${current_bindings[@]}" | grep -q -x "$potential_path"; then 143 | free_slot_path="$potential_path" 144 | break 145 | fi 146 | done 147 | 148 | if [[ -z "$free_slot_path" ]]; then 149 | error "Could not find a free keybinding slot (checked custom0-99)." 150 | exit 1 151 | fi 152 | 153 | info "Creating binding '$name' at $free_slot_path" 154 | local schema="${KB_SCHEMA_PREFIX}:${free_slot_path}" 155 | gsettings set "$schema" name "$name" 156 | gsettings set "$schema" binding "$bind" 157 | gsettings set "$schema" command "$cmd" 158 | 159 | # Add the new path to our array and mark that we made a change 160 | current_bindings+=("$free_slot_path") 161 | bindings_changed=true 162 | done 163 | 164 | if $bindings_changed; then 165 | # Write the updated array back to dconf 166 | set_dconf_array current_bindings 167 | success "Installation complete." 168 | cat << EOF 169 | 170 | GLOBAL HOTKEYS INSTALLED: 171 | ${KEYBINDING_BINDS[0]} -> ${KEYBINDING_NAMES[0]} 172 | ${KEYBINDING_BINDS[1]} -> ${KEYBINDING_NAMES[1]} 173 | 174 | If hotkeys do not work immediately, try restarting GNOME Shell: 175 | - On Xorg: Press Alt+F2, type 'r', and press Enter. 176 | - On Wayland: Log out and log back in. 177 | EOF 178 | else 179 | success "All keybindings were already installed. No changes made." 180 | fi 181 | } 182 | 183 | uninstall_keybindings() { 184 | info "Uninstalling OBS global hotkeys..." 185 | 186 | # Read current keybindings into a bash array 187 | mapfile -t current_bindings < <(get_dconf_array) 188 | declare -a bindings_to_keep=() 189 | local bindings_changed=false 190 | 191 | for path in "${current_bindings[@]}"; do 192 | # Skip empty paths that might result from parsing 193 | [[ -z "$path" ]] && continue 194 | local name 195 | name=$(gsettings get "${KB_SCHEMA_PREFIX}:${path}" name 2>/dev/null || echo "") 196 | 197 | # Check if the fetched name is in our list of names to uninstall 198 | local found_to_remove=false 199 | for name_to_remove in "${KEYBINDING_NAMES[@]}"; do 200 | if [[ "$name" == "$name_to_remove" ]]; then 201 | found_to_remove=true 202 | break 203 | fi 204 | done 205 | 206 | if $found_to_remove; then 207 | warn "Removing keybinding '$name' at path $path" 208 | local schema="${KB_SCHEMA_PREFIX}:${path}" 209 | # Resetting each key removes the custom value at that path. 210 | gsettings reset "$schema" name >/dev/null 2>&1 || true 211 | gsettings reset "$schema" binding >/dev/null 2>&1 || true 212 | gsettings reset "$schema" command >/dev/null 2>&1 || true 213 | bindings_changed=true 214 | else 215 | # This is not our keybinding, so we keep it. 216 | bindings_to_keep+=("$path") 217 | fi 218 | done 219 | 220 | if $bindings_changed; then 221 | # Write the filtered array back to dconf 222 | set_dconf_array bindings_to_keep 223 | success "Uninstallation complete." 224 | else 225 | success "No relevant OBS keybindings found to uninstall." 226 | fi 227 | } 228 | 229 | # ---- Preflight & Main Execution ----------------------------------------------- 230 | require gsettings 231 | require dconf 232 | require obs-cmd 233 | 234 | case "${1:-}" in 235 | install) 236 | install_keybindings 237 | ;; 238 | uninstall) 239 | uninstall_keybindings 240 | ;; 241 | -h | --help) 242 | usage 243 | ;; 244 | *) 245 | usage 246 | ;; 247 | esac 248 | -------------------------------------------------------------------------------- /esoteric/init-btrfs-rootfs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################################### 3 | # BTRFS Root FileSystem Fixer # 4 | # # 5 | # Automatically moves your root (/) BTRFS into subvolumes. # 6 | # # 7 | # / => /@rootfs # 8 | # /home => /@home # 9 | # /snaps => /@snaphots (for hourly/daily backups. # 10 | # See: cron.hourly/btrfs-snapshot( # 11 | # /important => /@important (for high-frequency snapshots that # 12 | # are not auto-pruned.) # 13 | # # 14 | # Part of HopeSeekr's BashScripts Collection # 15 | # https://github.com/hopeseekr/BashScripts/ # 16 | # # 17 | # Copyright © 2020 Theodore R. Smith # 18 | # GPG Fingerprint: 4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 # 19 | # # 20 | # License: Creative Commons Attribution v4.0 International # 21 | ######################################################################### 22 | 23 | ## NOTE: This script is particularly designed for Arch Linux. 24 | ## This is because Arch unoptimally creates the root BTRFS / 25 | ## in subvolid=5 instead of a @rootfs subvolume. 26 | ## 27 | ## Because of that, it makes restoring snapshots extremely painful. 28 | ## 29 | ## NOTE: This has been successfully tested on Ubuntu 20.04, Arch Linux, and Fedora 32. 30 | 31 | # @see https://misc.flogisoft.com/bash/tip_colors_and_formatting 32 | # https://archive.is/Z9Q2Q 33 | BOLD="\033[1m" 34 | RESET="\033[0m" 35 | FG_DEFAULT="\033[39m" 36 | FG_RED="\033[31m" 37 | FG_YELLOW="\033[93m" 38 | FG_WHITE="\033[97m" 39 | BG_RED="\033[101m" 40 | BG_BLUE="\033[44m" 41 | 42 | # @see https://serverfault.com/a/37836/56309 43 | if [[ $(/usr/bin/id -u) -ne 0 ]]; then 44 | echo "Error: You *MUST* run this as sudo or root!" 45 | echo "" 46 | exit 99 47 | fi 48 | 49 | # Start installer if the /@rootfs hasn't been created yet. 50 | if [[ ! -f /newrootfs ]]; then 51 | echo "STOP!!!" 52 | echo "" 53 | echo "Are you *certain* that you have backed up everything of importance?" 54 | echo "This is a risky operation: even though it is relatively safe, if something" 55 | echo "unexpected happens, you'll quite possibly lose your entire /." 56 | echo "" 57 | echo "You really ought to have a rescue disk handy before you begin!" 58 | echo "See https://www.system-rescue.org/Download/" 59 | echo "" 60 | # @see https://stackoverflow.com/a/1885534/430062 61 | read -p "Continue? (y) " -n 1 -r 62 | echo "" 63 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 64 | exit 0 65 | fi 66 | 67 | touch /true-root 68 | # @see https://unix.stackexchange.com/a/612283/15780 69 | DISK=$(mount | grep " / type" | awk '{print $1}') 70 | PART_UUID=$(blkid -o value -s UUID ${DISK}) 71 | echo "ROOT UUID: ${PART_UUID}" 72 | 73 | cd / 74 | echo "Creating the @rootfs subvolume..." 75 | btrfs subvolume create @rootfs 76 | ROOT_SID=$(btrfs subvol list / | grep @rootfs | awk '{print $2}') 77 | echo subvolid=${ROOT_SID} > "/@rootfs/newrootfs" 78 | 79 | echo "Shallow-copying / to /@rootfs (subvol=${ROOT_SID})... (Takes about 1-5 minutes)" 80 | echo "NOTE: /var/log/journal errors are normal and unavoidable due to how systemd works." 81 | cp -aixp --reflink=always bin etc lib* opt usr var root /@rootfs 82 | 83 | echo "Creating the @home, @snapshots and @important subvolumes..." 84 | btrfs subvolume create @home 85 | 86 | echo "Copying files from /home to /@home... (Takes a while)" 87 | cp -avpix /mnt/home/* /@home/ 88 | mv /home /home.orig 89 | mkdir /home.new 90 | mount -t btrfs -o subvol=@home UUID=${PART_UUID} /home 91 | btrfs subvolume create @snapshots 92 | btrfs subvolume create @important 93 | 94 | mkdir -p /media/true-root 95 | 96 | echo "OK! Step 1 is complete!" 97 | echo "" 98 | echo "Now, for Step 2, add the following entries to /etc/stab." 99 | echo -e "${BOLD}Be *absolutely certain* that you replace the subvol= option for /" 100 | echo -e "${BOLD}or your system will get bricked.${RESET}" 101 | echo "" 102 | echo " UUID=${PART_UUID} /home btrfs rw,noatime,ssd,autodefrag,space_cache,subvol=/@home 0 0" 103 | echo " UUID=${PART_UUID} /snaps btrfs rw,noatime,ssd,autodefrag,space_cache,subvol=/@snapshots 0 0" 104 | echo " UUID=${PART_UUID} / btrfs rw,noatime,ssd,autodefrag,space_cache,subvol=/@rootfs 0 0" 105 | echo " UUID=${PART_UUID} /important btrfs rw,noatime,ssd,autodefrag,space_cache,subvol=/@important 0 0" 106 | echo " UUID=${PART_UUID} /media/true-root btrfs rw,noatime,ssd,subvolid=5,noauto 0 0" 107 | echo "" 108 | echo "You really ought to have a rescue disk handy before you begin!" 109 | echo "See https://www.system-rescue.org/Download/" 110 | echo "" 111 | echo "If all goes to hell, enter into a rescue prompt and run the following:" 112 | echo "" 113 | echo " sudo -s" 114 | echo " mkdir -p /media/recovery /media/recovery-root" 115 | echo " mount -o ssd,subvolid=5 /media/recovery" 116 | echo " mount -o ssd,subvol=@rootfs /media/recovery-root" 117 | echo " # Figure out which one is more messed up:" 118 | echo " arch-chroot /media/recovery-root # This should work!" 119 | echo " # if not: " 120 | echo " cp -a --reflink=always /media/recovery/* /media/recovery-root/" 121 | echo " # Retry:" 122 | echo " arch-chroot /media/recovery-root # This should work!" 123 | echo "" 124 | echo "If you cannot access arch-chroot via either /emdia/recovery or /media/recovery-root," 125 | echo "then we have accidentally hosed your system, and I apologize. Create a GitHub issue!" 126 | echo "" 127 | echo "You also need to ensure that 'subvol=@rootfs' is set in /media/recovery-root/etc/fstab" 128 | echo "" 129 | echo "If you want/need to reset the state of your system BEFORE you run Step 3, do this:" 130 | echo "" 131 | echo " umount /home" 132 | echo " btrfs subvol delete --commit-each @rootfs @home @snapshots @important" 133 | echo "" 134 | echo "This is heavy surgery for any live system. Good luck!" 135 | echo "" 136 | echo "When your system reboots, run this script for a second time to finish the mibration process." 137 | echo "Otherwise, you will have a duplicated / lurking around taking up space." 138 | else 139 | echo "Wooho!! You've made it to the final Stage 3!!" 140 | echo "" 141 | echo -e "\e[1mThis stage is the ${FG_RED}*destructive*${FG_DEFAULT} one. We're going to be ${FG_RED}DELETING${FG_DEFAULT} all the old duplicate stuff!" 142 | echo -e "If you didn't make backups before, you really ought to now!${RESET}" 143 | echo "" 144 | # @see https://stackoverflow.com/a/1885534/430062 145 | read -p "Continue? (y) " -n 1 -r 146 | echo "" 147 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 148 | echo "No problem. I'll be here when you're ready!" 149 | exit 0 150 | fi 151 | 152 | echo "First, we're going to delete your old /home on the master /..." 153 | read -p "Ready? (y) " -n 1 -r 154 | echo "" 155 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 156 | echo "No problem. I'll be here when you're ready!" 157 | exit 0 158 | fi 159 | 160 | mount /media/true-root 161 | 162 | if [[ ! -f /media/true-root/true-root ]]; then 163 | echo "Error: Something very strange has gone wrong and this is not the true-root (subvolid=5)" 164 | echo " Or this script is just very confused! Bailing for your own safety!" 165 | exit 2 166 | fi 167 | 168 | cd /media/true-root/@rootfs/ 169 | rm -rvf home/* 170 | rm -rvf home.new 171 | 172 | if [[ -d /home.new ]]; then 173 | echo "Error: For some reason, deleting 'home' from your @rootfs in 'true-root' did not affect" 174 | echo " the 'home' in /." 175 | echo " You really ought to open up a bug report https://github.com/hopeseekr/BashScripts" 176 | exit 3 177 | fi 178 | 179 | echo "Now, we're going to delete every duplicate file in 'true-root'." 180 | read -p "Are you positively certain? (y) " -n 1 -r 181 | echo "" 182 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 183 | echo "Well! Can't say I blame you!" 184 | exit 0 185 | fi 186 | 187 | read -p "You're still doubly sure? (We provide 0 warranty!) (y) " -n 1 -r 188 | echo "" 189 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 190 | echo "Whew! Wipe the sweat off your brow!" 191 | echo "It's better to be safe than possibly sorry when rm -rvf'ing /!!" 192 | exit 0 193 | fi 194 | 195 | echo -e "${BOLD}${FG_YELLOW}# ============================================================= #" 196 | echo -e "${BOLD}${FG_YELLOW}# Buckle your seatbelt, Dorothy! Cuz Kansas...is going bye-bye! ${FG_YELLOW}#" 197 | echo -e "# ${RESET}-- Cypher of New Zion (circa ~2199 CE) ${FG_YELLOW} # " 198 | echo -e "${BOLD}${FG_YELLOW}# ============================================================= #${RESET}" 199 | echo "" 200 | echo -e "${BOLD}=== HIT ${FG_RED}CTRL+C${FG_DEFAULT} NOW IF YOU DO NOT TRUST MY PROGRAMMING!! ===${RESET}" 201 | echo "" 202 | i=10 203 | while (( i >= 1 )); do 204 | echo -n $(( i-- ))... 205 | sleep 1 206 | done 207 | echo "" 208 | echo -e "${BOLD} ${BG_RED}${FG_WHITE} B O O M ${RESET}" 209 | echo "Well! Hopefully not! ::fingers-crossed::" 210 | sleep 3 211 | echo -e "${BOLD}${BG_BLUE} Welcome... To the real world! ${RESET} [of properly-configured btrfs subvolumes!]" 212 | fi 213 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | GG### v1.0.0 @ 2020-09-30 2 | * **[2020-09-20 10:47:15 CDT]** - Initial: 3 | * **[ls-by-min]** Sort `ls` by file size, descending. 4 | * **[stream-to-youtube.sh]** Live Screencast directly to YouTube from the CLI (via ffmpeg). 5 | * **[watermark.sh]** Easily embed your own image watermark onto videos (via ffmpeg). 6 | * **[x265.sh]** Effectively highly-compress (with acceptable visual loss) videos to H.265 HEVC using Intel's VAAPI. 7 | * **[2020-09-23 14:59:35 CDT]** - **[x265]** Pass all extra parameters directly to ffmpeg. 8 | * **[2020-09-23 15:00:18 CDT]** - Added a utility to download all of a user's or organization's GitHub repos. 9 | * **[2020-09-23 15:01:15 CDT]** - **[sync-watch]** See how much data is waiting to be written to disks. 10 | * **[2020-09-23 15:01:37 CDT]** - **[random.sh]** Picks a random file in a given directory. 11 | * **[2020-09-30 12:41:40 CDT]** - Only prompt once per reboot for the sudo password. 12 | * **[2020-09-30 23:19:40 CDT]** - Added a utility to automatically move your btrfs / into subvolumes. 13 | * **[2020-09-30 23:37:15 CDT]** - Moved the clear-cache script to ensure it runs first. 14 | * **[2020-09-30 23:37:53 CDT]** - **[clear-cache]** Also delete Pacman's downloads. 15 | * **[2020-09-30 23:41:29 CDT]** - **[purge-locales]** Automatically remove every non-English locale daily. 16 | * **[2020-09-30 23:42:34 CDT]** - **[btrfs-snapshot]** Take snapshots of / every hour and every day. 17 | 18 | ### v1.1.0 @ 2020-10-01 19 | * **[2020-10-01 00:23:15 CDT]** - **[x265.sh]** Use VAAPI for encoding using Intel graphics cards. 20 | * **[2020-10-01 00:23:32 CDT]** - Added copyright headers. 21 | * **[2020-10-01 00:30:00 CDT]** - Created a README.md. 22 | * **[2020-10-01 00:37:07 CDT]** - **[random-file]** renamed for more clarity. 23 | * **[2020-10-01 00:44:34 CDT]** - Flushed out the README. 24 | * **[2020-10-01 00:49:44 CDT]** - **[bash_rc.aliases]** Added a whole slew of webdev aliases. 25 | * **[2020-10-01 00:58:00 CDT]** - **[bash_rc.aliases]** Added some more descriptions. 26 | * **[2020-10-01 01:10:26 CDT]** - **[.gitconfig]** Added a whole bunch of my git aliases. tag: v1.0.0 27 | * **[2020-10-01 08:13:51 CDT]** - **[clear-cache]** Use nullfs if it is available. 28 | * **[2020-10-01 08:17:42 CDT]** - **[clear-cache]** Delete broken symlinks in the ~/.cache directories. 29 | * **[2020-10-01 08:18:38 CDT]** - **[btrfs-snapshot]** Improved the handling of snapshots at day terminuses. 30 | 31 | ### v1.1.1 @ 2020-10-05 32 | * **[2020-10-01 08:38:03 CDT]** - **[changelog-maker-lite]** Added a utility for making pretty CHANGELOGs. 33 | 34 | ### v2.0.0 @ 2020-10-22 35 | * **[2020-10-20 20:10:54 CDT]** - **[arch-pacman-dupe-cleaner]** Utility for resolving "error: duplicated database entry 36 | 'foo'" 37 | * **[2020-10-22 15:59:58 CDT]** - **[Major]** Relicensed to the Creative Commons Attribution v4.0 International License. 38 | * **[2020-10-22 01:01:01 CDT]** - **[git-mtime]** Restores the file modification times of your git workdir to the repo's. 39 | * **[2020-10-22 04:34:32 CDT]** - **[ssh-keyphrase-only-once]** Only type in your SSH keyphrase once per boot. 40 | * **[2020-10-22 16:22:09 CDT]** - **[turn-off-monitors]** Easily turn off all of your monitors via the CLI. 41 | * **[2020-10-22 18:36:34 CDT]** - Added a Table of Contents to the README. 42 | 43 | Behavioral changes: 44 | * **[2020-10-22 14:53:09 CDT]** - **[changelog-maker-lite]** Now outputs Markdown lists. 45 | 46 | ### v2.1.0 @ 2020-10-23 47 | * **[2020-10-23 12:31:25 CDT]** - **[m]** Refactored to use /usr/bin/env shebang. 48 | * **[2020-10-23 12:39:46 CDT]** - **[wifi-autorun-on-connect]** Autorun a script when you connect to a Wifi hotspot. 49 | * **[2020-10-23 18:40:30 CDT]** - Translated the README into Chinese and Hindi to support 3 Billion people. 50 | 51 | ## v2.1.1 @ 2020-12-27 52 | * **[2020-12-26 17:00:11 CST]** - **[git-shallow-pull]** Added a utility to shallow update git. 53 | * **[2020-12-27 12:51:48 CST]** - **[git-change-author]** Easily bulk change the author's name and email in a git repo. 54 | 55 | ## v2.2.0 @ 2021-06-04 56 | * **[2021-06-04 09:08:31 CDT]** - **[ssh-autologin]** Automatically set up SSH autologins. 57 | 58 | ## v2.3.0 @ 2021-10-30 59 | * **[2021-10-30 07:00:24 CDT]** - **[clone-github-repos]** Fixed a syntax error. 60 | * **[2021-10-30 07:03:45 CDT]** - **[gitconfig]** Added a git alias for easily renaming tags. 61 | * **[2021-10-30 07:05:00 CDT]** - **[ssh-autologin]** Fixed the exec permission. 62 | 63 | ## v2.4.0 @ 2024-01-13 64 | * **[2024-01-13 13:55:42 CST]** Added a Bash alias to have `free` show the total memory in MiBs. 65 | * **[2024-01-13 13:56:25 CST]** Added a Bash alias `ps-date` to show the full date timestamp of a long-running process. 66 | * **[2024-01-13 13:56:54 CST]** Added a Bash alias to hide all of the snap /dev/loop devices from df. 67 | * **[2024-01-13 13:59:23 CST]** [changelog-maker-lite] Bold the timestamps. 68 | 69 | ## v2.4.1 @ 2024-01-14 70 | * **[2024-01-14 22:31:40 CST]** [arch-pacman-dupe-cleaner] Fixed the script for modern Arch. 71 | 72 | ## v2.5.0 @ 2024-01-19 73 | * **[2020-10-14 15:53:34 CST]** [american-date] A utility to print out dates in the American format 74 | ('Fri, 19 January 2024 05:49:20 CST'). 75 | * **[2024-01-15 09:14:17 CST]** [tar-sorted] Create tar files automatically sorted by file name. 76 | * **[2023-11-17 11:22:42 CST]** [git-commit-at-modded-time] Use a file's modified time as the git time. 77 | * **[2024-01-19 06:17:20 CST]** [git-same-sig-time] Unifies the GPG signature time with the commit's time. 78 | 79 | ## v2.5.1 @ 2024-08-09 80 | * **[2024-03-17 05:21:56 CDT]** [btrfs-snapshot] Added support for hourly snapshots of /code. 81 | * **[2024-08-09 18:43:46 CDT]** [tar-sorted] Added support for --zstd archives. 82 | 83 | ## v2.6.0 @ 2024-08-12 84 | 85 | * **[2024-08-12 02:06:41 CDT]** Added a .bash_profile. 86 | * **[2024-08-12 02:09:39 CDT]** [launch-browser] Launch Chrome-based browsers in native Wayland. 87 | * **[2024-08-12 02:35:43 CDT]** [framework/is_root] Added a function for determining root access. 88 | * **[2024-08-12 03:16:29 CDT]** [framework/wait_until_mouse_or_keyboard_event] Block execution until a key is pressed, 89 | the mouse is moved, or a mouse button is clicked. 90 | * **[2024-08-12 03:22:17 CDT]** [bash_rc.aliases] Added an alias to make `watch` honor ~/.bashrc aliases. 91 | * **[2024-08-12 03:22:50 CDT]** [bash_rc.aliases] Replace ssh with mosh, if it is installed. 92 | * **[2024-08-12 03:29:03 CDT]** [gitconfig] Automatically time out git when websites are not reachable. 93 | * **[2024-08-12 03:38:25 CDT]** [arch-pacman-dupe-cleaner] Require superuser to run. 94 | * **[2024-08-12 03:41:30 CDT]** [turn-off-monitors] A CLI script for turning off monitors in Wayland for Gnome and KDE. 95 | * **[2024-08-12 06:08:57 CDT]** Majorly cleaned up the README and translated to Chinese, Hindi, and Spanish. 96 | 97 | ## v2.6.1 @ 2024-08-23 98 | 99 | * **[2024-08-23 07:09:10 CDT]** [git-same-sig-time] Added proper support for time zones different than the user's computer. 100 | 101 | ## v2.7.0 @ 2024-08-24 102 | 103 | * **[2024-08-24 12:06:32 CDT]** [bash-timer] Added the bash-timer project. 104 | 105 | ## v2.7.1 @ 2024-09-01 106 | 107 | * **[2024-08-29 07:49:02 CDT]** [bash-timer] Added support for locales with the comma decimal separator. 108 | * **[2024-09-01 12:31:33 CDT]** [bash-timer] Properly pad ms to 3 digits. 109 | * **[2024-09-01 12:41:07 CDT]** [bash-timer] Greatly refactored ns math to properly calculate seconds and ms. 110 | * **[2024-09-10 02:20:14 CDT]** [bash-timer] Fixed the "value too great" error. 111 | * **[2024-09-11 02:22:12 CDT]** [bash-timer] Supported non-decimal locales, properly in Bash v5. 112 | 113 | ## v2.7.2 @ 2025-01-27 114 | 115 | * **[2024-12-07 19:41:20 CST]** [turn-off-monitors] Added compatibility to KDE Desktop. 116 | * **[2025-01-19 21:58:12 CET]** [turn-off-monitors] Added support for touch screen wakeup. 117 | * **[2025-01-25 19:18:27 CST]** [wifi-show-password] Added a utility to show the current wifi password. 118 | * **[2025-01-27 10:56:29 CST]** [arch-upgrade-postgres] Added a utility to automatically upgrade postgres on Arch Linux. 119 | 120 | ## v2.7.3 @ 2025-05-12 121 | 122 | * **[2025-02-08 15:26:11 CDT]** [arch-upgrade-postgres] Made it better. 123 | * **[2025-05-12 08:27:50 CDT]** [sync-watch] Use system "watch" instead of any Bash alias. 124 | 125 | ## v2.8.0 @ 2025-07-29 126 | 127 | * **[2025-07-29 20:24:07 CDT]** [my-ip] Added a little utility to discover your private IP address. 128 | 129 | ## v3.0.0 @ 2025-09-30 130 | 131 | * **[2025-08-09 14:16:16 CDT]** [obs-global-hotkeys] A utility to add Global Hotkeys for OBS on Wayland. 132 | * **[2025-08-09 18:46:09 CDT]** [tar-stats] Iteration 1 - Added compression support. 133 | * **[2025-08-09 18:47:24 CDT]** [tar-stats] Iteration 2 - Added extraction support. 134 | * **[2025-08-09 21:40:28 CDT]** [tar-stats] Iteration 3 - Added auto-detection for the compression utility to use when decompressing. 135 | * **[2025-08-10 13:06:25 CDT]** [turn-off-nvidia] Added a utility to turn off Nvidia graphics card to greatly extend battery life. 136 | * **[2025-08-12 12:57:09 CDT]** [git-shift-time] Added a utility to shift the timestamp of git commits. 137 | * **[2025-08-12 14:35:28 CDT]** [tar-stats] Split into multiple source code files and added a compilation step. 138 | * **[2025-08-12 14:38:35 CDT]** [turn-off-nvidia] Power savings for Xorg. 139 | * **[2025-08-12 15:47:45 CDT]** [turn-off-nvidia] Major rewrite with Wayland support and modern power management 140 | * **[2025-08-19 05:26:47 CDT]** [turn-off-nvidia] More refactoring. 141 | * **[2025-09-28 06:29:56 CDT]** [git-filter-copy] A utility to copy workdirs complying with .giattributes export restrictions. 142 | * **[2025-09-30 18:51:33 CDT]** [tar-stats] Much more work, but still not ready. 143 | * **[2025-09-30 20:52:11 CDT]** [image-mp3-to-video] Combines an image with an mp3 to produce an H264 video. 144 | * **[2025-09-30 20:54:18 CDT]** [turn-off-nvidia] Added the README documentation. 145 | 146 | ## v3.1.0 @ 2025-11-14 147 | 148 | * **[2025-10-11 00:54:45 CDT]** [git-change-author] Now no need for rebase window and maintains commit timestamps like `git-same-sig-time`. 149 | * **[2025-11-14 17:33:38 CST]** [git-commit-at-modded-time] Added support for more than 1 file. 150 | * **[2025-11-14 22:58:55 CST]** [change-maker-lite] Dramatically refactored + added correct DST time zone. 151 | * **[2025-11-14 23:01:12 CST]** [clone-github-repos] Do not shallow clone. 152 | 153 | -------------------------------------------------------------------------------- /tar-stats-src/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | IFS=$'\n\t' 3 | 4 | # Configuration: adjust if needed 5 | TAR_BIN="$(command -v tar)" 6 | STAT_BIN="/tmp/tar-stats" # the "other" tar you want to compare 7 | COPY_SRC="./tar-stats" # copy this to /tmp/ as requested 8 | FAKETIME_CMD="$(command -v faketime || true)" 9 | 10 | if [[ ! -x "${TAR_BIN:-}" ]]; then 11 | echo "Error: system tar not found or not executable." >&2 12 | exit 2 13 | fi 14 | if [[ ! -x "${COPY_SRC}" ]]; then 15 | echo "Error: ${COPY_SRC} not found or not executable. Place your tar-stats binary there." >&2 16 | exit 2 17 | fi 18 | # Copy tar-stats to /tmp/ 19 | cp -f "$COPY_SRC" "$STAT_BIN" 20 | chmod +x "$STAT_BIN" 21 | 22 | if [[ ! -x "$STAT_BIN" ]]; then 23 | echo "Error: failed to copy ${COPY_SRC} to ${STAT_BIN}" >&2 24 | exit 2 25 | fi 26 | 27 | if [[ -z "$FAKETIME_CMD" ]]; then 28 | echo "Warning: faketime not found. Tests that rely on faketime mtimes will still attempt to run but won't set file mtimes." >&2 29 | # continue, faketime just won't be used 30 | fi 31 | 32 | # Temp workspace 33 | WORKDIR="$(mktemp -d /tmp/tar-create-tests.XXXXXX)" 34 | trap 'rm -rf "$WORKDIR"' EXIT 35 | 36 | PASS=0 37 | FAIL=0 38 | 39 | # Helpers 40 | die() { echo "ERROR: $*" >&2; exit 1; } 41 | info() { echo "==> $*"; } 42 | 43 | # Normalize tar listing lines: remove leading ./ if present and ignore empty lines 44 | list_normalize() { 45 | sed 's#^\./##' | sed '/^$/d' | sort 46 | } 47 | 48 | # Run one test case 49 | # Args: 50 | # $1 = test name 51 | # $2 = function that sets up files under $TEST_DIR (called with argument $TEST_DIR) 52 | # $3... = additional tar args to test (applied to both tar binaries) 53 | run_test() { 54 | local name="$1"; shift 55 | local setup_func="$1"; shift 56 | local tar_args=( "$@" ) 57 | 58 | info "TEST: $name" 59 | local testdir="$WORKDIR/${name// /_}.src" 60 | mkdir -p "$testdir" 61 | # Call setup function to populate testdir 62 | "$setup_func" "$testdir" 63 | 64 | local arch1="$WORKDIR/${name// /_}.sys.tar" 65 | local arch2="$WORKDIR/${name// /_}.stat.tar" 66 | local list1="$WORKDIR/${name// /_}.sys.list" 67 | local list2="$WORKDIR/${name// /_}.stat.list" 68 | local ex1="$WORKDIR/${name// /_}.sys.ex" 69 | local ex2="$WORKDIR/${name// /_}.stat.ex" 70 | mkdir -p "$ex1" "$ex2" 71 | 72 | # Create archives using both tar binaries. Use -C to avoid './' ambiguity. 73 | # The tar_args are appended; ensure -cf is used in front. 74 | ( cd "$testdir" && "$TAR_BIN" -cf "$arch1" "${tar_args[@]}" . ) 75 | ( cd "$testdir" && "$STAT_BIN" -cf "$arch2" "${tar_args[@]}" . ) 76 | 77 | # List members (use system tar to inspect both archives); normalize 78 | "$TAR_BIN" -tf "$arch1" | list_normalize > "$list1" 79 | "$TAR_BIN" -tf "$arch2" | list_normalize > "$list2" 80 | 81 | local ok=true 82 | if ! diff -u "$list1" "$list2" >/dev/null 2>&1; then 83 | ok=false 84 | echo "FAIL: member list differs" 85 | echo "=== ${name} : system tar list ===" 86 | sed -n '1,200p' "$list1" 87 | echo "=== ${name} : /tmp/tar-stats list ===" 88 | sed -n '1,200p' "$list2" 89 | fi 90 | 91 | # Extract and compare files, mtimes and modes 92 | ( cd "$ex1" && "$TAR_BIN" -xf "$arch1" ) 93 | ( cd "$ex2" && "$TAR_BIN" -xf "$arch2" ) 94 | 95 | # Build file lists of regular files and symlinks found (relative paths, normalized) 96 | local files1 files2 symlinks1 symlinks2 97 | files1=$(cd "$ex1" && find . -type f -print | sed 's#^\./##' | sort) 98 | files2=$(cd "$ex2" && find . -type f -print | sed 's#^\./##' | sort) 99 | symlinks1=$(cd "$ex1" && find . -type l -print | sed 's#^\./##' | sort) 100 | symlinks2=$(cd "$ex2" && find . -type l -print | sed 's#^\./##' | sort) 101 | 102 | # Compare file sets 103 | if ! diff -u <(printf "%s\n" $files1) <(printf "%s\n" $files2) >/dev/null 2>&1; then 104 | ok=false 105 | echo "FAIL: regular file sets differ" 106 | echo "--- files in sys" 107 | printf "%s\n" $files1 108 | echo "--- files in stat" 109 | printf "%s\n" $files2 110 | fi 111 | 112 | # Compare symlink sets 113 | if ! diff -u <(printf "%s\n" $symlinks1) <(printf "%s\n" $symlinks2) >/dev/null 2>&1; then 114 | ok=false 115 | echo "FAIL: symlink sets differ" 116 | echo "--- symlinks in sys" 117 | printf "%s\n" $symlinks1 118 | echo "--- symlinks in stat" 119 | printf "%s\n" $symlinks2 120 | fi 121 | 122 | # For each regular file, compare sha256, mode, mtime 123 | while IFS= read -r f; do 124 | [[ -z "$f" ]] && continue 125 | local f1="$ex1/$f" 126 | local f2="$ex2/$f" 127 | # Sha256 128 | local s1 s2 129 | s1=$(sha256sum "$f1" | awk '{print $1}') 130 | s2=$(sha256sum "$f2" | awk '{print $1}') 131 | if [[ "$s1" != "$s2" ]]; then 132 | ok=false 133 | echo "FAIL: content differs for file $f" 134 | echo " sys: $s1" 135 | echo " stat: $s2" 136 | fi 137 | # Mode (octal) 138 | local m1 m2 139 | m1=$(stat -c %a "$f1") 140 | m2=$(stat -c %a "$f2") 141 | if [[ "$m1" != "$m2" ]]; then 142 | ok=false 143 | echo "FAIL: mode differs for $f (sys:$m1 stat:$m2)" 144 | fi 145 | # Mtime (epoch) 146 | local t1 t2 147 | t1=$(stat -c %Y "$f1") 148 | t2=$(stat -c %Y "$f2") 149 | if [[ "$t1" != "$t2" ]]; then 150 | ok=false 151 | echo "FAIL: mtime differs for $f (sys:$t1 stat:$t2)" 152 | fi 153 | done < <(printf "%s\n" $files1) 154 | 155 | # Symlink targets comparison 156 | while IFS= read -r s; do 157 | [[ -z "$s" ]] && continue 158 | local s1="$ex1/$s" 159 | local s2="$ex2/$s" 160 | local t1 t2 161 | t1=$(readlink "$s1" || true) 162 | t2=$(readlink "$s2" || true) 163 | if [[ "$t1" != "$t2" ]]; then 164 | ok=false 165 | echo "FAIL: symlink target differs for $s (sys:$t1 stat:$t2)" 166 | fi 167 | done < <(printf "%s\n" $symlinks1) 168 | 169 | # Hardlink check: detect pairs in original testdir with same inode and verify extraction preserved hardlink 170 | # Original hardlink sets: find -same-file is tricky; instead in the setup functions we create known link pairs with names "hl_src" and "hl_dst" 171 | if [[ -f "$testdir/hl_src" ]] || [[ -f "$testdir/hl_dst" ]]; then 172 | # If present, test the extraction for hardlink preservation 173 | if [[ -f "$ex1/hl_src" && -f "$ex1/hl_dst" ]]; then 174 | local i1a i1b i2a i2b 175 | i1a=$(stat -c %i "$ex1/hl_src") 176 | i1b=$(stat -c %i "$ex1/hl_dst") 177 | i2a=$(stat -c %i "$ex2/hl_src") 178 | i2b=$(stat -c %i "$ex2/hl_dst") 179 | if [[ "$i1a" != "$i1b" ]]; then 180 | ok=false 181 | echo "FAIL: system tar did not preserve hardlink between hl_src and hl_dst" 182 | fi 183 | if [[ "$i2a" != "$i2b" ]]; then 184 | ok=false 185 | echo "FAIL: /tmp/tar-stats did not preserve hardlink between hl_src and hl_dst" 186 | fi 187 | else 188 | ok=false 189 | echo "FAIL: expected hardlink files not present after extraction" 190 | fi 191 | fi 192 | 193 | if $ok; then 194 | echo "PASS: $name" 195 | PASS=$((PASS+1)) 196 | else 197 | echo "=== FAILED: $name ===" 198 | FAIL=$((FAIL+1)) 199 | fi 200 | 201 | echo "" 202 | } 203 | 204 | # Setup functions for different tests 205 | # Each receives the testdir as $1 and should create the needed files/dirs relative to $1. 206 | # They should avoid root-only operations. 207 | touch_with_time() { 208 | local ts="$1"; shift 209 | local path="$1"; shift 210 | if [[ -n "$FAKETIME_CMD" ]]; then 211 | faketime "$ts" sh -c "cat > \"$path\" <<'EOF' 212 | $2 213 | EOF" 214 | else 215 | # fallback: just write file (mtime will be now) 216 | cat > "$path" <<'EOF' 217 | $2 218 | EOF 219 | fi 220 | } 221 | # Simpler helper to write contents and set mtime via faketime (if available) 222 | write_with_mtime() { 223 | local testdir="$1"; shift 224 | local rel="$1"; shift 225 | local ts="$1"; shift 226 | local content="$1"; shift 227 | mkdir -p "$(dirname "$testdir/$rel")" 228 | if [[ -n "$FAKETIME_CMD" ]]; then 229 | faketime "$ts" sh -c "printf '%s' \"\$1\" > \"$testdir/$rel\"" -- "$content" 230 | else 231 | printf '%s' "$content" > "$testdir/$rel" 232 | fi 233 | } 234 | 235 | setup_basic_files() { 236 | local d="$1" 237 | mkdir -p "$d/dirA" 238 | write_with_mtime "$d" "file_a.txt" "2020-01-02 03:04:05" "hello world" 239 | write_with_mtime "$d" "dirA/file_b.txt" "2020-02-03 04:05:06" "inside dir" 240 | mkdir -p "$d/emptydir" 241 | : > "$d/emptyfile" 242 | chmod 0644 "$d/emptyfile" 243 | chmod 0755 "$d/dirA" 244 | } 245 | 246 | setup_symlinks() { 247 | local d="$1" 248 | mkdir -p "$d" 249 | echo "data" > "$d/orig" 250 | ln -s "orig" "$d/link_to_orig" 251 | ln -s "../orig" "$d/dirlink" 252 | mkdir -p "$d/subdir" 253 | ln -s "../orig" "$d/subdir/rel" 254 | } 255 | 256 | setup_hardlinks() { 257 | local d="$1" 258 | mkdir -p "$d" 259 | echo "hardlinkcontent" > "$d/hl_src" 260 | ln "$d/hl_src" "$d/hl_dst" 261 | } 262 | 263 | setup_space_and_unicode() { 264 | local d="$1" 265 | mkdir -p "$d" 266 | write_with_mtime "$d" "file with spaces.txt" "2018-05-06 07:08:09" "spaced" 267 | write_with_mtime "$d" "unicodé-☃.txt" "2018-05-06 07:08:09" "unicode" 268 | mkdir -p "$d/dir with space" 269 | echo "x" > "$d/dir with space/inner" 270 | } 271 | 272 | setup_longnames() { 273 | local d="$1" 274 | mkdir -p "$d" 275 | local longname="$(printf 'a%.0s' {1..200})" 276 | echo "long" > "$d/$longname" 277 | # also a deep nested path 278 | mkdir -p "$d/$(printf 'p%.0s/' {1..30})" 279 | echo "deep" > "$d/$(printf 'p%.0s/' {1..30})deepfile" 280 | } 281 | 282 | setup_sparse() { 283 | local d="$1" 284 | mkdir -p "$d" 285 | # create sparse file: create file with a hole by seeking 286 | # Using dd with seek 287 | dd if=/dev/zero of="$d/sparse" bs=1 count=0 seek=$((10*1024*1024)) 2>/dev/null || truncate -s 10M "$d/sparse" 288 | # add a small footer to ensure content at end 289 | echo "end" >> "$d/sparse" 290 | } 291 | 292 | setup_modes_and_mtimes() { 293 | local d="$1" 294 | mkdir -p "$d" 295 | # Create files with different modes and mtimes. Use faketime to set mtimes. 296 | if [[ -n "$FAKETIME_CMD" ]]; then 297 | faketime "2001-01-01 00:00:00" sh -c 'printf "one" > "$1"' -- "$d/mode1" 298 | faketime "2002-02-02 02:02:02" sh -c 'printf "two" > "$1"' -- "$d/mode2" 299 | else 300 | printf "one" > "$d/mode1" 301 | printf "two" > "$d/mode2" 302 | fi 303 | chmod 0640 "$d/mode1" 304 | chmod 0755 "$d/mode2" 305 | } 306 | 307 | # Now register tests 308 | run_test "basic files and dirs" setup_basic_files 309 | run_test "symlinks preservation" setup_symlinks 310 | run_test "hardlinks preservation" setup_hardlinks 311 | run_test "names with spaces and unicode" setup_space_and_unicode 312 | run_test "long filenames and deep paths" setup_longnames 313 | run_test "sparse file handling" setup_sparse 314 | run_test "modes and mtimes" setup_modes_and_mtimes 315 | 316 | # Summary 317 | echo "SUMMARY: PASS=$PASS FAIL=$FAIL" 318 | if [[ $FAIL -ne 0 ]]; then 319 | exit 1 320 | fi 321 | exit 0 -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 | # HopeSeekr 的 Bash 脚本集 2 | 3 | 用于简化生活的实用工具。 4 | 5 | 这是我日常使用的脚本,或在新的系统安装中使用的脚本,大大改善了我使用 Linux 的体验! 6 | 7 | ![Hope 的标志](https://avatars2.githubusercontent.com/u/50506504?s=200&v=4) 8 | 9 | 它们根据对我提供的效用进行排序。 10 | 11 | 目录(分类) 12 | ==================== 13 | * **超级生产力** 14 | * **[bash_profile](#bash_profile)** — 针对高级用户的高质量 `.bash_profile` 和 `.bashrc`。 15 | * **[bash_rc.aliases](#bash_rcaliases)** — 高效的 .bashrc 别名和函数,用于提升生活质量。 16 | * **[bash-timer](#bash-timer)** — 在bash中轻松为每个命令添加人类可读的执行时间! 17 | * **[ssh-autologin](#ssh-autologin)** — 自动设置 SSH 自动登录。 18 | * **[ssh-keyphrase-only-once](#ssh-keyphrase-only-onceinstaller)** — 每次启动时仅请求一次 SSH 密码短语。 19 | * **[gitconfig](#gitconfig)** — 高效的 Git CLI 配置。 20 | * **Git 生活质量** 21 | * **[git-change-author](#git-change-author)** — 轻松批量更改 git 仓库中的作者姓名和电子邮件。 22 | * **[git-commit-at-modded-time](#git-commit-at-modded-time)** — 使用文件修改时间作为 git 提交时间。 23 | * **[git-filter-copy](#git-filter-copy)** — 复制 Git 工作目录,同时保留清洁状态和本地修改,并遵守 .gitattributes 导出规则。 24 | * **[git-mtime](#git-mtime-git-modified-time-restorer)** — 将每个文件的修改时间恢复到 git 仓库历史记录中。 25 | * **[git-same-sig-time](#git-same-sig-time)** — 统一 GPG 签名时间与提交时间。 26 | * **[git-shallow-pull](#esotericgit-shallow-pull)** — 浅层更新 `git clone --depth 1` 的仓库。 27 | * **[git-shift-time](#git-shift-time)** — 调整 Git 提交时间戳,以分钟或特定日期为单位。 28 | * **CronTabs** 29 | * **[cron.daily/00_clear-cache](#crondaily00_clear-cache)** — 每晚清除无用的缓存文件。 30 | * **[cron.daily/01_purge-locales](#crondaily01_purge-locales)** — 删除无用的国际化文件。 31 | * **[cron.hourly/btrfs-snapshot](#cronhourlybtrfs-snapshot)** — 每小时备份你的 BTRFS / 。 32 | * **[cron.daily/btrfs-snapshot](#crondailybtrfs-snapshot)** — 每天备份你的 BTRFS / 。 33 | * **[cron.hourly/php-clean-tmp](#cronhourlyphp-clean-tmp)** — 在繁忙的服务器上清理 PHP 临时文件。 34 | * **生活质量** 35 | * **[launch-browser](#launch-browser)** — 用于启动浏览器的实用脚本,可在完整的 Wayland 模式下启动它们。 36 | * **[ls-by-min](#ls-by-min)** — 按文件大小降序排序 `ls` 输出。 37 | * **[my-ip](#my-ip)** — 轻松获取该机器的公共IP地址。 38 | * **[obs-global-hotkeys](#obs-global-hotkeys)** — OBS 全局快捷键安装程序(GNOME Wayland + Xorg) 39 | * **[random-file](#random-file)** — 随机选择并显示一个文件或目录。 40 | * **[sync-watch](#sync-watch)** — 显示需要写入磁盘的 MB 数量的更新屏幕。 41 | * **[tar-sorted](#tar-sorted)** — 自动按文件名排序创建 tar 文件。 42 | * **[turn-off-monitors](#turn-off-monitors)** — 通过命令行关闭所有显示器(CLI)。 43 | * **[turn-off-nvidia](#turn-off-nvidia)** — 为 Xorg/Wayland 提供全面的 NVIDIA 节能模式,优先使用 RTD3。 44 | * **[wifi-show-password](#wifi-show-password)** — 显示当前连接的wifi密码。 45 | * **精彩脚本** 46 | * [american-date](#american-date) — 一个用于打印美国日期格式的工具。 47 | * [changelog-maker-lite](#changelog-maker-lite) — 根据提交历史快速创建美观的变更日志。 48 | * **[image-mp3-to-video](#image-mp3-to-video)** — 将图像与 MP3 结合以创建 H264 视频。 49 | * [stream-to-youtube](#stream-to-youtube) — 直接从命令行直播到 YouTube。 50 | * [sudoers.d/00_prompt_once](#sudoersd00_prompt_once) — 每次启动时仅请求一次 sudo 密码。 51 | * [watermark.sh](#watermarksh) — 轻松在视频中嵌入自定义水印。 52 | * [wifi-autorun-on-connect](#wifi-autorun-on-connectinstaller) — 连接到 WiFi 热点时自动运行脚本。 53 | * [x265.sh](#x265sh) — 使用 Intel 显卡通过 VAAPI 转码到 h265 HEVC。 54 | * **神秘工具** 55 | * [arch-pacman-dupe-cleaner](#esotericarch-pacman-dupe-cleaner) — 清除 Arch Linux Pacman 中的重复条目。 56 | * [arch-upgrade-postgres](#esotericarch-upgrade-postgres) — 在 Arch Linux 中自动将 PostgreSQL 从一个版本升级到下一个版本。 57 | * [init-btrfs-rootfs](#esotericinit-btrfs-rootfs) — 将所有内容放入组织良好的 BTRFS 子卷,并启用快照。 58 | * [clone-github-repos.php](#esotericclone-github-reposphp) — 下载用户/组织的所有 GitHub 仓库。 59 | * **Bash 框架** 60 | * [is_root](#is_root) — 提供 `is_root` 函数以确定当前用户是否具有 root 访问权限。 61 | * [wait_until_mouse_or_keyboard_event](#wait_until_mouse_or_keyboard_event) — 阻止程序执行,直到按下键盘键、移动鼠标或单击鼠标按钮。 62 | 63 | * [许可证](#license) — 创作共用 v4.0 国际许可证 64 | * [关于作者](#about-the-author) 65 | 66 | ## bash_profile 67 | 68 | 将许多 Bash 脚本结合在一起,创建一个超级强大的系统。 69 | 70 | ## bash_rc.aliases 71 | 72 | 这可能是项目中最有价值的一部分! 73 | 74 | 这些数十个别名使在 Linux 上工作和开发 Web 应用程序更加高效和优化。 75 | 76 | 快去查看文件! [bash_rc.aliases](bash_rc.aliases) 77 | 78 | ## bash-timer 79 | 80 | 在bash中轻松为每个命令添加人类可读的执行时间! 81 | 82 | 时间将显示在左下角,紧靠您的$PS1左侧。 83 | 84 | ![bash-timer image](https://user-images.githubusercontent.com/1125541/93687425-7c392100-fa83-11ea-9d36-cacbe03cc725.png) 85 | ``` 86 | 2 days 05:02:11.33 # A very long process 87 | ``` 88 | 89 | **安装方法:** 90 | 91 | ```bash 92 | curl https://raw.githubusercontent.com/hopeseekr/bash-timer/v1.5.0/install | bash 93 | ``` 94 | 95 | [**查看README.md**](bash-timer/README.md)了解更多信息。 96 | 97 | ## ssh-autologin 98 | 99 | 自动设置(如果需要)SSH 私钥并将其安装在远程服务器上(如果需要,还将创建 .ssh 目录)。 100 | 101 | ## ssh-keyphrase-only-once.installer 102 | 103 | 正确配置你的 OpenSSH 客户端并在 `~/.bash_profile` 中安装 `ssh-agent`,这样你只需在每次登录时为任何使用 `ssh-agent` 的操作输入一次 SSH 密码短语。 104 | 105 | ## gitconfig 106 | 107 | 一个高度优化的 .gitconfig 文件,适用于经验丰富的软件开发人员,以提高他们的日常生产力。 108 | 109 | 安装在 ~/.gitconfig 中。 110 | 111 | git pretty 112 | git ll 113 | 114 | 显示一个带有标准 ISO 日期的漂亮多彩日志: 115 | 116 | ![git pretty](https://user-images.githubusercontent.com/1125541/94773688-e904c300-0381-11eb-878a-d0396d2bf102.png) 117 | 118 | git fix 119 | 120 | 快速跳转以将最近的两个提交合并为一个提交。 121 | 122 | git alterego 123 | 124 | 快速切换当前仓库以使用你的主 AlterEgo 账户提交先前的更改。 125 | 126 | git ego 127 | 128 | git alterego 的反义词。 129 | 130 | git resign 131 | 132 | 快速批量重新签署从指定 hash 到最近的提交。 133 | 134 | 这在 GPG 密钥到期并需要更新然后重新签署时特别有用。 135 | 136 | git c: checkout 137 | git cp: cherry-pick 138 | git cpm: cherry-pick for Merge commits 139 | 140 | 实用的别名,使使用 git 更加高效。 141 | 142 | git shows 143 | 144 | 在 git 日志中显示每个提交的 GPG 签名。 145 | 146 | ## git-change-author 147 | 148 | 轻松批量更改 git 仓库中任意提交的作者姓名和电子邮件。 149 | 150 | 使用方法: `git-change-author "你的名字" "email@地址" [SHA1]` 151 | 152 | ## git-commit-at-modded-time 153 | 154 | 使用文件修改时间作为 git 提交时间。 155 | 156 | 示例: 157 | 158 | $ ls -l american-date 159 | #-rwxrwxr-x+ 1 1MB Oct 14 2020 american-date 160 | $ ./git-commit-at-modded-time american-date 161 | $ git pretty american-date 162 | 7462b66 G 2020-10-14 15:53:34 -0500 Theodore R. Smith 163 | 164 | ## git-filter-copy 165 | 166 | 该实用程序旨在复制目录,同时考虑源目录是否为 Git 仓库。如果源目录是 Git 仓库,它将从工作目录导出项目当前状态,包括任何由 Git 跟踪且未在 .gitattributes 中标记为“导出忽略”的本地修改。 167 | 168 | 这意味着会复制已提交到仓库的文件(或已本地修改但仍有跟踪的文件),同时排除从未提交或通过 .gitattributes 明确忽略的文件。这确保您仅获得用于本地测试的相关且预期中的项目文件。 169 | 170 | ## git-mtime Git 修改时间恢复器 171 | 172 | 将工作目录中每 173 | 174 | 个文件的修改时间恢复到它们在 git 仓库历史记录中的时间。 175 | 176 | 当你签出一个仓库并执行此操作时,你的工作目录将从: 177 | 178 | -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 changelog-maker-lite* 179 | -rw-rw-r--+ 1 tsmith users 1MB Oct 22 01:58 CHANGELOG.txt 180 | -rw-rw-r--+ 1 tsmith users 1MB Oct 22 01:58 gitconfig 181 | -rwxrwxr-x+ 1 tsmith users 1MB Oct 22 01:58 git-mtime 182 | 183 | 变为: 184 | 185 | -rwxrwxr-x+ 1 tsmith users 1MB Oct 1 08:38 changelog-maker-lite* 186 | -rw-rw-r--+ 1 tsmith users 1MB Oct 1 01:10 gitconfig 187 | -rwxrwxr-x+ 1 tsmith users 1MB Sep 30 23:19 git-mtime 188 | 189 | ## git-same-sig-time 190 | 191 | 统一 GPG 签名时间与提交时间。 192 | 193 | 之前: 194 | 195 | gpg: Signature made Fri 19 Jan 2024 06:50:44 AM CST 196 | gpg: using RSA key 4BF826131C3487ACD28F2AD8EB24A91DD6125690 197 | gpg: Good signature from "Theodore R. Smith " [ultimate] 198 | 43578ec G 2024-01-16 01:52:41 -0600 Theodore R. Smith [m] Updated the packages and exclusion lists. HEAD 199 | gpg: Signature made Fri 19 Jan 2024 06:50:25 AM CST 200 | gpg: using RSA key 4BF826131C3487ACD28F2AD8EB24A91DD6125690 201 | gpg: Good signature from "Theodore R. Smith " [ultimate] 202 | 8ab4104 G 2024-01-15 08:27:07 -0600 Theodore R. Smith Upgraded to PHP 8.3. 203 | 204 | 之后: 205 | 206 | gpg: Signature made Tue 16 Jan 2024 01:52:41 AM CST 207 | gpg: using RSA key 4BF826131C3487ACD28F2AD8EB24A91DD6125690 208 | gpg: Good signature from "Theodore R. Smith " [ultimate] 209 | 515e36b G 2024-01-16 01:52:41 -0600 Theodore R. Smith [m] Updated the packages and exclusion lists. HEAD 210 | gpg: Signature made Mon 15 Jan 2024 08:27:07 AM CST 211 | gpg: using RSA key 4BF826131C3487ACD28F2AD8EB24A91DD6125690 212 | gpg: Good signature from "Theodore R. Smith " [ultimate] 213 | 22c5040 G 2024-01-15 08:27:07 -0600 Theodore R. Smith Upgraded to PHP 8.3. 214 | 215 | ## git-shift-time 216 | 217 | 在保留 GPG 签名的同时,以分钟或特定日期为单位调整 Git 提交时间戳。 218 | 219 | 此实用程序可让您将提交时间向前或向后移动指定的分钟数,或将它们设置为特定的日期/时间。 它对于修复在不同时区工作后的提交时间戳、将提交对齐到特定时间或纠正时间错误的提交非常有用。 220 | 221 | ```bash 222 | git-shift-time <提交哈希> <分钟偏移量> [目标日期] 223 | ``` 224 | 225 | ## tar-sorted 226 | 227 | 自动按文件名排序创建 tar 文件。 228 | 229 | 这在随机排序的文件系统(如 ext4)中特别有用。 230 | 231 | 在 Bettergist Collector 中,我们使用此方法来大致估算压缩和提取多个 GB 包含数百万文件的档案所需的时间。 232 | 233 | 这是 `tar` 的直接替代,并使用相同的参数。 234 | 235 | 可选地,你可以将函数直接安装在你的 ~/.bashrc 中。 236 | 237 | ## cron.daily/00_clear-cache 238 | 239 | - 清空非 root 用户的 ~/.cache 目录。 240 | - 清空 Pacman 的包缓存目录。 241 | 242 | 此操作可以选择性地使用 [nullfsvfs](https://github.com/abbbi/nullfsvfs) 来限制 SSD 的磨损。 243 | 244 | ## cron.daily/01_purge-locales 245 | 246 | 每天删除所有非 EN 的国际化文件。 247 | 248 | 通常可以节省 400 至 1000 MB 的空间。 249 | 250 | ## cron.hourly/btrfs-snapshot 251 | 252 | 每小时整点拍摄 BTRFS 的 / 快照。 253 | 254 | ## cron.daily/btrfs-snapshot 255 | 256 | 清理前一天的每小时快照,同时保留每日快照。 257 | 258 | ## cron.hourly/php-clean-tmp 259 | 260 | 清理无用的 PHP 临时文件,这在繁忙的服务器上非常有用。 261 | * 最近一小时内未修改的旧会话文件。 262 | * phpunit 的临时文件。 263 | * phpstan 的临时文件。 264 | 265 | ## american-date 266 | 267 | 用于打印美国格式日期的工具 268 | 269 | 周五, 2024 年 1 月 19 日 05:49:20 CST 270 | 271 | ## changelog-maker-lite 272 | 273 | 轻松根据简洁的 git 提交记录创建 [CHANGELOG](CHANGELOG.md): 274 | 275 | [2020-10-01 00:23:15 CDT] — [x265.sh] 使用 Intel 显卡的 VAAPI 进行编码。 276 | [2020-10-01 00:30:00 CDT] — 创建了 README.md。 277 | [2020-10-01 00:37:07 CDT] — [random-file] 为了更清晰的重命名。 278 | [2020-10-01 00:44:34 CDT] — 充实了 README。 279 | [2020-10-01 00:49:44 CDT] — [bash_rc.aliases] 添加了一系列 Web 开发别名。 280 | [2020-10-01 00:58:00 CDT] — [bash_rc.aliases] 添加了更多描述。 281 | [2020-10-01 01:10:26 CDT] — [.gitconfig] 添加了大量 git 别名。 标签: v1.0.0 282 | [2020-10-01 08:17:42 CDT] — [clear-cache] 删除 ~/.cache 目录中的损坏符号链接。 283 | 284 | ## image-mp3-to-video 285 | 286 | **image-mp3-to-video** 是一个脚本,可将图像与 MP3 音频文件结合起来创建 H264 视频。它适用于创建带有背景音乐的幻灯片演示或将图像转换为带有附加音频的视频。该过程简单易行,并允许调整屏幕上图像的时间、帧率和最终视频质量等参数。 287 | 288 | ## launch-browser 289 | 290 | 1. 检测用户是否在运行 Wayland。 如果是这样,对于基于 Chromium 的浏览器,它将传递必要的标志,以便在 Wayland 原生模式(非 XWayland)下运行,以获得更高的性能。 291 | 2. 它将始终使用 Gnome 密钥环启动,这样每次切换到 KDE 时,你不会丢失所有 cookie 和持久登录信息,反之亦然。 292 | 293 | ## ls-by-min 294 | 295 | 返回按文件大小降序排列的文件列表,文件大小至少为 X MB。 296 | 297 | ## my-ip 298 | 299 | 轻松获取该机器的公共IP地址。 300 | 301 | ## obs-global-hotkeys 302 | 303 | **obs-global-hotkeys** 自动化在 GNOME 环境(包括 Wayland 和 Xorg)中为 OBS Studio 设置真正的全局快捷键,解决了常见的问题,即 OBS 快捷键仅在应用程序获取焦点时才有效。它通过智能地查找 GNOME 媒体键配置中的可用插槽来安全地安装可定制的按键绑定,不会覆盖现有的用户自定义快捷键。该过程是幂等操作——这意味着重复安装不会导致条目重复——并自动配置两个默认的快捷键:**Ctrl+F8** 用于切换录制,**Ctrl+F9** 用于暂停/恢复播放。在运行之前,请确保已安装 `obs-cmd`(来自 `obs-cli` 软件包),以启用 OBS 的命令行控制。 304 | 305 | 要使用该脚本,请运行 `./obs-global-hotkeys.sh install` 添加快捷键,或运行 `./obs-global-hotkeys.sh uninstall` 清除它们。安装程序会动态地保留未使用的按键绑定插槽(例如,`custom0`、`custom1`),同时保留您现有的 GNOME 快捷键并添加 OBS 功能。如果快捷键在安装后没有立即激活,请通过 Alt+F2 > `r`(Xorg)重新启动 GNOME Shell,或注销/登录(Wayland)。卸载命令仅准确地删除由该脚本创建的绑定,不影响其他自定义按键绑定。两次操作都需要 `gsettings` 和 `dconf`,这是 GNOME 中的标准组件。 306 | 307 | ## random-file 308 | 309 | 在当前目录或工作目录中随机选择一个文件。 310 | 311 | ## stream-to-youtube 312 | 313 | 直接从命令行通过 YouTube 直播屏幕(通过 ffmpeg)! 314 | 315 | ## sudoers.d/00_prompt_once 316 | 317 | 这将使 `sudo` 在一次启动过程中仅要求输入一次密码。 它不会通过不同的终端请求,并且不会过期。 重启后会自动过期。 318 | 319 | ## sync-watch 320 | 321 | 显示仍需写入磁盘的 MB 数量。 322 | 323 | 每 5.0s: grep -e Dirty: -e Writeback: /proc/meminfo 324 | asus-z13: 周五 2024 年 1 月 19 日 07:09:32 325 | 326 | Dirty: 751552 kB 327 | Writeback: 0 kB 328 | 329 | ## turn-off-monitors 330 | 331 | 通过命令行轻松关闭所有显示器。 332 | 333 | 当你想要离开并不那么担心锁屏时使用。 334 | 335 | ## turn-off-nvidia 336 | 337 | **全面的 NVIDIA GPU 电源管理脚本** 338 | 339 | **turn-off-nvidia** 是一个全面的 Bash 脚本,用于在 Linux 上管理 NVIDIA dGPU 电源状态,作为少数几个完全支持 AMD CPU 上的 NVIDIA GPU(在 Wayland 上)的电力管理解决方案之一,同时在 Xorg 上也运行完美。它优先采用实时 D3 电源管理 (RTD3) 作为现代方法,允许离线 GPU 在空闲时自动进入深度睡眠状态(D3cold),显著降低笔记本电脑的功耗和热量。 340 | 341 | 该脚本提供了多种配置方法,包括适用于 ASUS 笔记本电脑和 Wayland 的 supergfxctl、envycontrol、optimus-manager,以及如 bbswitch 和 acpi_call 等传统选项。它具有智能系统检测、全面诊断、独立于发行版的软件包管理(pacman/AUR、apt、dnf、zypper)、PRIME 卸载设置以实现按需 GPU 使用、实时电源监控和安全回滚选项。配备了广泛文档和 Wayland 特定指南,turn-off-nvidia 使混合图形笔记本电脑获得最佳电池寿命变得简单易行。 342 | 343 | ## watermark.sh 344 | 345 | 通过 ffmpeg 为视频添加水印。 346 | 347 | ## wifi-autorun-on-connect.installer 348 | 349 | 安装一个 NetworkManager 脚本,在连接到特定 WiFi 网络时自动运行。 350 | 351 | ## wifi-show-password 352 | 353 | 显示当前连接的wifi密码。 354 | 355 | ## x265.sh 356 | 357 | 通过使用 Intel 显卡的 ffmpeg 转码到 x265 HEVC。 358 | 359 | # 神秘工具 360 | 361 | ## esoteric/arch-pacman-dupe-cleaner 362 | 363 | 帮助自动修复 Arch Linux Pacman 数据库中的重复条目。 364 | 365 | 旨在解决罕见问题:“错误:重复的数据库条目‘foo’” 366 | 367 | ## esoteric/arch-upgrade-postgres 368 | 369 | 自动执行 Arch Linux wiki 指令以将 PostgreSQL 从一个版本升级到另一个版本。 370 | 371 | ## esoteric/git-shallow-pull 372 | 373 | 浅层更新 `git clone --depth 1` 的仓库。 374 | 375 | ## esoteric/init-btrfs-rootfs 376 | 377 | 在 Arch Linux 上,btrfs 配置将所有文件放在 / 上。 这极大地限制了适当管理系统的能力,subvol=5 除了子卷以外什么也没有。 378 | 379 | 这个脚本做以下事情: 380 | 381 | * 创建新的根级别快照:`@rootfs` (/), `@snapshots`, `@important` 和 `@home`。 382 | * 将 `/` 的文件移动到 `/@rootfs`,将 `/home` 的文件移动到 `/home`,并创建新的目录 `/snaps` 和 `/important`。 383 | * 创建 `/media/true-root`,它会自动挂载到 `/ (subvol=5)`。 384 | * 与此项目中的自动快照 `cron.d` 作业集成 385 | * 每天在 `/snaps/daily/YYYY-MM-DD` 中对 `/` 进行快照。 386 | * **每小时**在 `/snaps/important/YYYY-MM-DD/HH` 中对 `/important` 进行快照。 387 | * `/important` 配置为启用压缩。 388 | * 将 `/home` 配置为基于每个用户的磁盘总空间的 10% 应用配额,并为每个用户的磁盘总空间应用 50% 的配额。 389 | * `/home` 快照配置为启用 390 | * `/home` 为用户的 `.*` 文件启用了 7 天的循环快照,除非是 `缓存` 目录(使用 [cron.daily/00_clear-cache] 时)。 391 | 392 | ### 问题 393 | 394 | Arch Linux 将 / 的所有内容放在 BTRFS 的主子卷中 (ID=5)。 395 | 396 | 这意味着常见的快照和子卷创建任务,特别是回滚到以前的根子卷(例如功能性),非常困难,并且需要启动救援等。 397 | 398 | ### 解决方案 399 | 400 | 提供的解决方案暂时解决了这一情况,幸运的是,在无需救援光盘的情况下,在实时环境中完成。 然而,你确实应该事先准备好救援光盘。 401 | 402 | 为了神的爱,首先备份! 403 | ![btrfs-init-rootfs](https://user-images.githubusercontent.com/1125541/94771567-231f9600-037d-11eb-8032-50d2b5873f36.png) 404 | 405 | ## esoteric/clone-github-repos.php 406 | 407 | 自动下载用户或组织的所有 GitHub 仓库。 408 | 409 | ## Bash 框架 410 | 411 | ### is_root 412 | 413 | 独立运行时,它将回显“以 root 身份运行”或“未以 root 身份运行”。 414 | 415 | 作为函数调用时,它将返回 `true` 或 `false`。 416 | 417 | ## wait_until_mouse_or_keyboard_event 418 | 419 | 阻止正在执行的程序,直到按下键盘键、移动鼠标或按下鼠标按钮,无论窗口焦点如何。 420 | 421 | 它将回显以下内容之一:KEYBOARD_KEY、MOUSE_CLICKED 或 MOUSE_MOVED。 422 | 423 | 可以独立运行或作为函数运行。 424 | 425 | # 许可证 426 | 427 | 本项目采用 [Creative Commons Attribution License v4.0 International](LICENSE.cc-by.md) 进行许可。 428 | 429 | ![CC.by 许可证摘要](https://user-images.githubusercontent.com/1125541/93617603-cd6de580-f99b-11ea-9da4-f79c168c97df.png) 430 | 431 | # 关于作者 432 | 433 | [Theodore R. Smith](https://www.phpexperts.pro/]) 434 | GPG 指纹:4BF8 2613 1C34 87AC D28F 2AD8 EB24 A91D D612 5690 435 | CEO: PHP Experts, Inc. 436 | -------------------------------------------------------------------------------- /bash-timer/assets/bash-preexec.sh: -------------------------------------------------------------------------------- 1 | # bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions. 2 | # https://github.com/rcaloras/bash-preexec 3 | # 4 | # 5 | # 'preexec' functions are executed before each interactive command is 6 | # executed, with the interactive command as its argument. The 'precmd' 7 | # function is executed before each prompt is displayed. 8 | # 9 | # Author: Ryan Caloras (ryan@bashhub.com) 10 | # Forked from Original Author: Glyph Lefkowitz 11 | # 12 | # V0.5.x = 25 Feb 2024 13 | # 14 | 15 | # General Usage: 16 | # 17 | # 1. Source this file at the end of your bash profile so as not to interfere 18 | # with anything else that's using PROMPT_COMMAND. 19 | # 20 | # 2. Add any precmd or preexec functions by appending them to their arrays: 21 | # e.g. 22 | # precmd_functions+=(my_precmd_function) 23 | # precmd_functions+=(some_other_precmd_function) 24 | # 25 | # preexec_functions+=(my_preexec_function) 26 | # 27 | # 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND 28 | # to use preexec and precmd instead. Preexisting usages will be 29 | # preserved, but doing so manually may be less surprising. 30 | # 31 | # Note: This module requires two Bash features which you must not otherwise be 32 | # using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override 33 | # either of these after bash-preexec has been installed it will most likely break. 34 | 35 | # Tell shellcheck what kind of file this is. 36 | # shellcheck shell=bash 37 | 38 | # Make sure this is bash that's running and return otherwise. 39 | # Use POSIX syntax for this line: 40 | if [ -z "${BASH_VERSION-}" ]; then 41 | return 1; 42 | fi 43 | 44 | # We only support Bash 3.1+. 45 | # Note: BASH_VERSINFO is first available in Bash-2.0. 46 | if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then 47 | return 1 48 | fi 49 | 50 | # Avoid duplicate inclusion 51 | if [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then 52 | return 0 53 | fi 54 | bash_preexec_imported="defined" 55 | 56 | # WARNING: This variable is no longer used and should not be relied upon. 57 | # Use ${bash_preexec_imported} instead. 58 | # shellcheck disable=SC2034 59 | __bp_imported="${bash_preexec_imported}" 60 | 61 | # Should be available to each precmd and preexec 62 | # functions, should they want it. $? and $_ are available as $? and $_, but 63 | # $PIPESTATUS is available only in a copy, $BP_PIPESTATUS. 64 | # TODO: Figure out how to restore PIPESTATUS before each precmd or preexec 65 | # function. 66 | __bp_last_ret_value="$?" 67 | BP_PIPESTATUS=("${PIPESTATUS[@]}") 68 | __bp_last_argument_prev_command="$_" 69 | 70 | __bp_inside_precmd=0 71 | __bp_inside_preexec=0 72 | 73 | # Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install 74 | __bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install' 75 | 76 | # Fails if any of the given variables are readonly 77 | # Reference https://stackoverflow.com/a/4441178 78 | __bp_require_not_readonly() { 79 | local var 80 | for var; do 81 | if ! ( unset "$var" 2> /dev/null ); then 82 | echo "bash-preexec requires write access to ${var}" >&2 83 | return 1 84 | fi 85 | done 86 | } 87 | 88 | # Remove ignorespace and or replace ignoreboth from HISTCONTROL 89 | # so we can accurately invoke preexec with a command from our 90 | # history even if it starts with a space. 91 | __bp_adjust_histcontrol() { 92 | local histcontrol 93 | histcontrol="${HISTCONTROL:-}" 94 | histcontrol="${histcontrol//ignorespace}" 95 | # Replace ignoreboth with ignoredups 96 | if [[ "$histcontrol" == *"ignoreboth"* ]]; then 97 | histcontrol="ignoredups:${histcontrol//ignoreboth}" 98 | fi; 99 | export HISTCONTROL="$histcontrol" 100 | } 101 | 102 | # This variable describes whether we are currently in "interactive mode"; 103 | # i.e. whether this shell has just executed a prompt and is waiting for user 104 | # input. It documents whether the current command invoked by the trace hook is 105 | # run interactively by the user; it's set immediately after the prompt hook, 106 | # and unset as soon as the trace hook is run. 107 | __bp_preexec_interactive_mode="" 108 | 109 | # These arrays are used to add functions to be run before, or after, prompts. 110 | declare -a precmd_functions 111 | declare -a preexec_functions 112 | 113 | # Trims leading and trailing whitespace from $2 and writes it to the variable 114 | # name passed as $1 115 | __bp_trim_whitespace() { 116 | local var=${1:?} text=${2:-} 117 | text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters 118 | text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters 119 | printf -v "$var" '%s' "$text" 120 | } 121 | 122 | 123 | # Trims whitespace and removes any leading or trailing semicolons from $2 and 124 | # writes the resulting string to the variable name passed as $1. Used for 125 | # manipulating substrings in PROMPT_COMMAND 126 | __bp_sanitize_string() { 127 | local var=${1:?} text=${2:-} sanitized 128 | __bp_trim_whitespace sanitized "$text" 129 | sanitized=${sanitized%;} 130 | sanitized=${sanitized#;} 131 | __bp_trim_whitespace sanitized "$sanitized" 132 | printf -v "$var" '%s' "$sanitized" 133 | } 134 | 135 | # This function is installed as part of the PROMPT_COMMAND; 136 | # It sets a variable to indicate that the prompt was just displayed, 137 | # to allow the DEBUG trap to know that the next command is likely interactive. 138 | __bp_interactive_mode() { 139 | __bp_preexec_interactive_mode="on"; 140 | } 141 | 142 | 143 | # This function is installed as part of the PROMPT_COMMAND. 144 | # It will invoke any functions defined in the precmd_functions array. 145 | __bp_precmd_invoke_cmd() { 146 | # Save the returned value from our last command, and from each process in 147 | # its pipeline. Note: this MUST be the first thing done in this function. 148 | # BP_PIPESTATUS may be unused, ignore 149 | # shellcheck disable=SC2034 150 | 151 | __bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}") 152 | 153 | # Don't invoke precmds if we are inside an execution of an "original 154 | # prompt command" by another precmd execution loop. This avoids infinite 155 | # recursion. 156 | if (( __bp_inside_precmd > 0 )); then 157 | return 158 | fi 159 | local __bp_inside_precmd=1 160 | 161 | # Invoke every function defined in our function array. 162 | local precmd_function 163 | for precmd_function in "${precmd_functions[@]}"; do 164 | 165 | # Only execute this function if it actually exists. 166 | # Test existence of functions with: declare -[Ff] 167 | if type -t "$precmd_function" 1>/dev/null; then 168 | __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command" 169 | # Quote our function invocation to prevent issues with IFS 170 | "$precmd_function" 171 | fi 172 | done 173 | 174 | __bp_set_ret_value "$__bp_last_ret_value" 175 | } 176 | 177 | # Sets a return value in $?. We may want to get access to the $? variable in our 178 | # precmd functions. This is available for instance in zsh. We can simulate it in bash 179 | # by setting the value here. 180 | __bp_set_ret_value() { 181 | return ${1:+"$1"} 182 | } 183 | 184 | __bp_in_prompt_command() { 185 | 186 | local prompt_command_array IFS=$'\n;' 187 | read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}" 188 | 189 | local trimmed_arg 190 | __bp_trim_whitespace trimmed_arg "${1:-}" 191 | 192 | local command trimmed_command 193 | for command in "${prompt_command_array[@]:-}"; do 194 | __bp_trim_whitespace trimmed_command "$command" 195 | if [[ "$trimmed_command" == "$trimmed_arg" ]]; then 196 | return 0 197 | fi 198 | done 199 | 200 | return 1 201 | } 202 | 203 | # This function is installed as the DEBUG trap. It is invoked before each 204 | # interactive prompt display. Its purpose is to inspect the current 205 | # environment to attempt to detect if the current command is being invoked 206 | # interactively, and invoke 'preexec' if so. 207 | __bp_preexec_invoke_exec() { 208 | 209 | # Save the contents of $_ so that it can be restored later on. 210 | # https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702 211 | __bp_last_argument_prev_command="${1:-}" 212 | # Don't invoke preexecs if we are inside of another preexec. 213 | if (( __bp_inside_preexec > 0 )); then 214 | return 215 | fi 216 | local __bp_inside_preexec=1 217 | 218 | # Checks if the file descriptor is not standard out (i.e. '1') 219 | # __bp_delay_install checks if we're in test. Needed for bats to run. 220 | # Prevents preexec from being invoked for functions in PS1 221 | if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then 222 | return 223 | fi 224 | 225 | if [[ -n "${COMP_POINT:-}" || -n "${READLINE_POINT:-}" ]]; then 226 | # We're in the middle of a completer or a keybinding set up by "bind 227 | # -x". This obviously can't be an interactively issued command. 228 | return 229 | fi 230 | if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then 231 | # We're doing something related to displaying the prompt. Let the 232 | # prompt set the title instead of me. 233 | return 234 | else 235 | # If we're in a subshell, then the prompt won't be re-displayed to put 236 | # us back into interactive mode, so let's not set the variable back. 237 | # In other words, if you have a subshell like 238 | # (sleep 1; sleep 2) 239 | # You want to see the 'sleep 2' as a set_command_title as well. 240 | if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then 241 | __bp_preexec_interactive_mode="" 242 | fi 243 | fi 244 | 245 | if __bp_in_prompt_command "${BASH_COMMAND:-}"; then 246 | # If we're executing something inside our prompt_command then we don't 247 | # want to call preexec. Bash prior to 3.1 can't detect this at all :/ 248 | __bp_preexec_interactive_mode="" 249 | return 250 | fi 251 | 252 | local this_command 253 | this_command=$( 254 | export LC_ALL=C 255 | HISTTIMEFORMAT='' builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //' 256 | ) 257 | 258 | # Sanity check to make sure we have something to invoke our function with. 259 | if [[ -z "$this_command" ]]; then 260 | return 261 | fi 262 | 263 | # Invoke every function defined in our function array. 264 | local preexec_function 265 | local preexec_function_ret_value 266 | local preexec_ret_value=0 267 | for preexec_function in "${preexec_functions[@]:-}"; do 268 | 269 | # Only execute each function if it actually exists. 270 | # Test existence of function with: declare -[fF] 271 | if type -t "$preexec_function" 1>/dev/null; then 272 | __bp_set_ret_value "${__bp_last_ret_value:-}" 273 | # Quote our function invocation to prevent issues with IFS 274 | "$preexec_function" "$this_command" 275 | preexec_function_ret_value="$?" 276 | if [[ "$preexec_function_ret_value" != 0 ]]; then 277 | preexec_ret_value="$preexec_function_ret_value" 278 | fi 279 | fi 280 | done 281 | 282 | # Restore the last argument of the last executed command, and set the return 283 | # value of the DEBUG trap to be the return code of the last preexec function 284 | # to return an error. 285 | # If `extdebug` is enabled a non-zero return value from any preexec function 286 | # will cause the user's command not to execute. 287 | # Run `shopt -s extdebug` to enable 288 | __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command" 289 | } 290 | 291 | __bp_install() { 292 | # Exit if we already have this installed. 293 | if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then 294 | return 1; 295 | fi 296 | 297 | trap '__bp_preexec_invoke_exec "$_"' DEBUG 298 | 299 | # Preserve any prior DEBUG trap as a preexec function 300 | local prior_trap 301 | # we can't easily do this with variable expansion. Leaving as sed command. 302 | # shellcheck disable=SC2001 303 | prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}") 304 | unset __bp_trap_string 305 | if [[ -n "$prior_trap" ]]; then 306 | eval '__bp_original_debug_trap() { 307 | '"$prior_trap"' 308 | }' 309 | preexec_functions+=(__bp_original_debug_trap) 310 | fi 311 | 312 | # Adjust our HISTCONTROL Variable if needed. 313 | __bp_adjust_histcontrol 314 | 315 | # Issue #25. Setting debug trap for subshells causes sessions to exit for 316 | # backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash. 317 | # 318 | # Disabling this by default. It can be enabled by setting this variable. 319 | if [[ -n "${__bp_enable_subshells:-}" ]]; then 320 | 321 | # Set so debug trap will work be invoked in subshells. 322 | set -o functrace > /dev/null 2>&1 323 | shopt -s extdebug > /dev/null 2>&1 324 | fi; 325 | 326 | local existing_prompt_command 327 | # Remove setting our trap install string and sanitize the existing prompt command string 328 | existing_prompt_command="${PROMPT_COMMAND:-}" 329 | # Edge case of appending to PROMPT_COMMAND 330 | existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op 331 | existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only 332 | existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only 333 | __bp_sanitize_string existing_prompt_command "$existing_prompt_command" 334 | if [[ "${existing_prompt_command:-:}" == ":" ]]; then 335 | existing_prompt_command= 336 | fi 337 | 338 | # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've 339 | # actually entered something. 340 | PROMPT_COMMAND='__bp_precmd_invoke_cmd' 341 | PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command} 342 | if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then 343 | PROMPT_COMMAND+=('__bp_interactive_mode') 344 | else 345 | # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 346 | PROMPT_COMMAND+=$'\n__bp_interactive_mode' 347 | fi 348 | 349 | # Add two functions to our arrays for convenience 350 | # of definition. 351 | precmd_functions+=(precmd) 352 | preexec_functions+=(preexec) 353 | 354 | # Invoke our two functions manually that were added to $PROMPT_COMMAND 355 | __bp_precmd_invoke_cmd 356 | __bp_interactive_mode 357 | } 358 | 359 | # Sets an installation string as part of our PROMPT_COMMAND to install 360 | # after our session has started. This allows bash-preexec to be included 361 | # at any point in our bash profile. 362 | __bp_install_after_session_init() { 363 | # bash-preexec needs to modify these variables in order to work correctly 364 | # if it can't, just stop the installation 365 | __bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return 366 | 367 | local sanitized_prompt_command 368 | __bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}" 369 | if [[ -n "$sanitized_prompt_command" ]]; then 370 | # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0 371 | PROMPT_COMMAND=${sanitized_prompt_command}$'\n' 372 | fi; 373 | # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 374 | PROMPT_COMMAND+=${__bp_install_string} 375 | } 376 | 377 | # Run our install so long as we're not delaying it. 378 | if [[ -z "${__bp_delay_install:-}" ]]; then 379 | __bp_install_after_session_init 380 | fi; -------------------------------------------------------------------------------- /LICENSE.cc-by.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | ### Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 10 | 11 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 12 | 13 | ## Creative Commons Attribution 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | ### Section 1 – Definitions. 18 | 19 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 24 | 25 | d. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 26 | 27 | e. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 28 | 29 | f. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 30 | 31 | g. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 32 | 33 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 34 | 35 | i. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 36 | 37 | j. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 38 | 39 | k. __You__ means the individual or entity exercising the Licensed Rights under this Public License. __Your__ has a corresponding meaning. 40 | 41 | ### Section 2 – Scope. 42 | 43 | a. ___License grant.___ 44 | 45 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 46 | 47 | A. reproduce and Share the Licensed Material, in whole or in part; and 48 | 49 | B. produce, reproduce, and Share Adapted Material. 50 | 51 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 52 | 53 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 54 | 55 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 56 | 57 | 5. __Downstream recipients.__ 58 | 59 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 60 | 61 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 62 | 63 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 64 | 65 | b. ___Other rights.___ 66 | 67 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 68 | 69 | 2. Patent and trademark rights are not licensed under this Public License. 70 | 71 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 72 | 73 | ### Section 3 – License Conditions. 74 | 75 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 76 | 77 | a. ___Attribution.___ 78 | 79 | 1. If You Share the Licensed Material (including in modified form), You must: 80 | 81 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 82 | 83 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 84 | 85 | ii. a copyright notice; 86 | 87 | iii. a notice that refers to this Public License; 88 | 89 | iv. a notice that refers to the disclaimer of warranties; 90 | 91 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 92 | 93 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 94 | 95 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 96 | 97 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 98 | 99 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 100 | 101 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 102 | 103 | ### Section 4 – Sui Generis Database Rights. 104 | 105 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 106 | 107 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 108 | 109 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 110 | 111 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 112 | 113 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 114 | 115 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 116 | 117 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 118 | 119 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 120 | 121 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 122 | 123 | ### Section 6 – Term and Termination. 124 | 125 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 126 | 127 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 128 | 129 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 130 | 131 | 2. upon express reinstatement by the Licensor. 132 | 133 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 134 | 135 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 136 | 137 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 138 | 139 | ### Section 7 – Other Terms and Conditions. 140 | 141 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 142 | 143 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 144 | 145 | ### Section 8 – Interpretation. 146 | 147 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 148 | 149 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 150 | 151 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 152 | 153 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 154 | 155 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 156 | > 157 | > Creative Commons may be contacted at creativecommons.org 158 | --------------------------------------------------------------------------------