diff --git a/.bashrc b/.bashrc index fb768d7..2f33f1e 100644 --- a/.bashrc +++ b/.bashrc @@ -61,8 +61,7 @@ shopt -s lithist # Initialize various utilities and aliases -source "$HOME/.config/shell/utils.sh" -source "$HOME/.config/shell/aliases.sh" +source "$HOME/.config/shell/init.sh" diff --git a/.config/shell/README.md b/.config/shell/README.md index bf9c14b..7731f1c 100644 --- a/.config/shell/README.md +++ b/.config/shell/README.md @@ -1,3 +1,11 @@ # Shell-related Configuration This folder contains further files that are sourced by `bash` and `zsh`. + +[init.sh](https://gitlab.webartifex.biz/alexander/dotfiles/-/blob/main/.config/shell/init.sh) + contains the common initialization logic for all shells + and + integrates the two sub-folders + [aliases.d](https://gitlab.webartifex.biz/alexander/dotfiles/-/tree/main/.config/shell/aliases.d) + and + [utils.d](https://gitlab.webartifex.biz/alexander/dotfiles/-/tree/main/.config/shell/utils.d). diff --git a/.config/shell/aliases.d/README.md b/.config/shell/aliases.d/README.md new file mode 100644 index 0000000..81e8657 --- /dev/null +++ b/.config/shell/aliases.d/README.md @@ -0,0 +1,7 @@ +# Shell Aliases + +This folder contains various files that define aliases + to be used in the shell. + +Originally, they all were defined in one big "~/.config/shell/aliases.sh" file + but are now split across many smaller files here for clarity. diff --git a/.config/shell/aliases.d/files.sh b/.config/shell/aliases.d/files.sh new file mode 100644 index 0000000..9e20725 --- /dev/null +++ b/.config/shell/aliases.d/files.sh @@ -0,0 +1,59 @@ +# Make working with files more convenient + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + +# Avoid bad mistakes and show what happens +alias cp="cp --interactive --verbose" +alias ln='ln --interactive --verbose' +alias mv='mv --interactive --verbose' +alias rm='rm -I --preserve-root --verbose' + + +# Faster directory switching +alias cd..='cd ..' +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' +alias .....='cd ../../../..' + + +# Convenient defaults +alias mkdir='mkdir -p' +alias md='mkdir' +alias rmdir='rmdir --parents --verbose' +alias rd='rmdir' + + +# Convenient grepping +alias grep='grep --color=auto --exclude-dir={.cache,\*.egg-info,.git,.nox,.tox,.venv}' +alias egrep='egrep --color=auto --exclude-dir={.cache,\*.egg-info,.git,.nox,.tox,.venv}' +alias fgrep='fgrep --color=auto --exclude-dir={.cache,*.egg-info,.git,.nox,.tox,.venv}' + + +# Convenient searching +alias fdir='find . -type d -name' +alias ffile='find . -type f -name' + + +# Convenient listings +alias ls='ls --classify --color=auto --group-directories-first --human-readable --no-group --time-style=long-iso' +alias la='ls --almost-all' +alias lal='la -l' +alias ll='ls -l' +alias l.='ls --directory .*' +alias ll.='l. -l' + + +# More convenience with various other file-related utilities +alias df='df --human-readable' +alias du='du --human-readable' +alias diff='diff --color=auto --unified' +_command_exists colordiff && alias diff='colordiff --unified' +alias free='free --human --total' +alias less='less --chop-long-lines --ignore-case --LONG-PROMPT --no-init --status-column --quit-if-one-screen' +alias more='less' +alias tree='tree -C --dirsfirst' diff --git a/.config/shell/aliases.d/generic.sh b/.config/shell/aliases.d/generic.sh new file mode 100644 index 0000000..e03c52e --- /dev/null +++ b/.config/shell/aliases.d/generic.sh @@ -0,0 +1,67 @@ +# Generic shell aliases for bash and zsh + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + +_in_zsh() { + [ -n "$ZSH_VERSION" ] +} + + +# Re-run last command with sudo privileges +if _in_zsh; then + alias ,,='sudo $(fc -ln -1)' +else + alias ,,='sudo $(history -p !!)' +fi + + +# Convenient piping with zsh +if _in_zsh; then + alias -g B='| bat' + alias -g F='| fzf' + alias -g G='| grep' + alias -g H='| head' + alias -g L='| less' + alias -g T='| tail' + alias -g NE='2 > /dev/null' + alias -g NUL='> /dev/null 2>&1' +fi + + +# (Non-)obvious synonyms +alias cls='clear' +alias help='man' + + +# Various one-line utilities +alias datetime='date +"%Y-%m-%d %H:%M:%S %z (%Z)"' +alias datetime-iso='date --iso-8601=seconds' +alias dotfiles='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME' +alias external-ip="curl https://icanhazip.com" +alias external-ip-alt="curl https://ipinfo.io/ip\?token=cfd78a97e15ebf && echo" +alias external-ip-extended-infos="curl https://ipinfo.io/json\?token=cfd78a97e15ebf && echo" +alias speedtest="curl -s https://raw.githubusercontent.com/sivel/speedtest-cli/22210ca35228f0bbcef75a7c14587c4ecb875ab4/speedtest.py | python -" + + +# Fix common typos +_command_exists ifconfig && alias ipconfig='ifconfig' +_command_exists R && alias r='R' + + +# Use sane defaults +_command_exists exa && alias exa='exa --group-directories-first --git --time-style=long-iso' +_command_exists netstat && alias ports='netstat -tulanp' +_command_exists screenfetch && alias screenfetch='screenfetch -n' +alias uptime='uptime --pretty' +alias wget='wget --continue' + + +# Create shorter aliases for various utilities +_command_exists batcat && alias bat='batcat' +_command_exists fdfind && alias fd='fdfind' +_command_exists neofetch && alias nf='neofetch' +_command_exists ranger && alias rn='ranger' +_command_exists screenfetch && alias sf='screenfetch' diff --git a/.config/shell/aliases.d/git.sh b/.config/shell/aliases.d/git.sh new file mode 100644 index 0000000..10d1d0c --- /dev/null +++ b/.config/shell/aliases.d/git.sh @@ -0,0 +1,24 @@ +# All git aliases (with < 7 characters) become shell aliases with a "g" prefix + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + +if _command_exists git; then + alias g='git' + + for al in $(git internal-aliases); do + [ ${#al} -lt 7 ] && eval "alias g$al='git $al'" + done + + # Check if a 'main' branch exists in place of a 'master' branch + git_main_branch() { + if [[ -n "$(git branch --list main)" ]]; then + echo 'main' + else + echo 'master' + fi + } +fi diff --git a/.config/shell/aliases.d/python.sh b/.config/shell/aliases.d/python.sh new file mode 100644 index 0000000..126cd31 --- /dev/null +++ b/.config/shell/aliases.d/python.sh @@ -0,0 +1,25 @@ +# Make working with Python more convenient + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + +# Interactive shells +alias py='python' +alias bpy='bpython' +alias ipy='ipython' + + +if _command_exists poetry; then + alias pr='poetry run' +fi + + +if _command_exists pyenv; then + alias pyvenvs='pyenv virtualenvs --bare --skip-aliases' + alias pyver='pyenv version' + alias pyvers='pyenv versions --skip-aliases' + alias pywhich='pyenv which' +fi diff --git a/.config/shell/aliases.sh b/.config/shell/aliases.sh deleted file mode 100644 index 322714e..0000000 --- a/.config/shell/aliases.sh +++ /dev/null @@ -1,157 +0,0 @@ -# Shell aliases for bash and zsh - - -_command_exists() { - command -v "$1" 1>/dev/null 2>&1 -} - -_in_zsh() { - [ -n "$ZSH_VERSION" ] -} - - -# Re-run last command with sudo privileges -if _in_zsh; then - alias ,,='sudo $(fc -ln -1)' -else - alias ,,='sudo $(history -p !!)' -fi - - -# Convenient piping with zsh -if _in_zsh; then - alias -g B='| bat' - alias -g F='| fzf' - alias -g G='| grep' - alias -g H='| head' - alias -g L='| less' - alias -g T='| tail' - alias -g NE='2 > /dev/null' - alias -g NUL='> /dev/null 2>&1' -fi - - -# (Non-)obvious synonyms -alias cls='clear' -alias help='man' - - -# Avoid bad mistakes and show what happens -alias cp="cp --interactive --verbose" -alias ln='ln --interactive --verbose' -alias mv='mv --interactive --verbose' -alias rm='rm -I --preserve-root --verbose' - - -# Make working with files more convenient - -# Faster directory switching -alias cd..='cd ..' -alias ..='cd ..' -alias ...='cd ../..' -alias ....='cd ../../..' -alias .....='cd ../../../..' - -# Convenient defaults -alias mkdir='mkdir -p' -alias md='mkdir' -alias rmdir='rmdir --parents --verbose' -alias rd='rmdir' - -# Convenient grepping -alias grep='grep --color=auto --exclude-dir={.cache,\*.egg-info,.git,.nox,.tox,.venv}' -alias egrep='egrep --color=auto --exclude-dir={.cache,\*.egg-info,.git,.nox,.tox,.venv}' -alias fgrep='fgrep --color=auto --exclude-dir={.cache,*.egg-info,.git,.nox,.tox,.venv}' - -# Convenient searching -alias fdir='find . -type d -name' -alias ffile='find . -type f -name' - -# Convenient listings -alias ls='ls --classify --color=auto --group-directories-first --human-readable --no-group --time-style=long-iso' -alias la='ls --almost-all' -alias lal='la -l' -alias ll='ls -l' -alias l.='ls --directory .*' -alias ll.='l. -l' - -# More convenience with various other file-related utilities -alias df='df --human-readable' -alias du='du --human-readable' -alias diff='diff --color=auto --unified' -_command_exists colordiff && alias diff='colordiff --unified' -alias free='free --human --total' -alias less='less --chop-long-lines --ignore-case --LONG-PROMPT --no-init --status-column --quit-if-one-screen' -alias more='less' -alias tree='tree -C --dirsfirst' - - -# Make working with Python more convenient - -# Interactive shells -alias py='python' -alias bpy='bpython' -alias ipy='ipython' - -if _command_exists poetry; then - alias pr='poetry run' -fi - -if _command_exists pyenv; then - alias pyvenvs='pyenv virtualenvs --bare --skip-aliases' - alias pyver='pyenv version' - alias pyvers='pyenv versions --skip-aliases' - alias pywhich='pyenv which' -fi - - -# Various one-line utilities -alias datetime='date +"%Y-%m-%d %H:%M:%S %z (%Z)"' -alias datetime-iso='date --iso-8601=seconds' -alias dotfiles='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME' -alias external-ip="curl https://icanhazip.com" -alias external-ip-alt="curl https://ipinfo.io/ip\?token=cfd78a97e15ebf && echo" -alias external-ip-extended-infos="curl https://ipinfo.io/json\?token=cfd78a97e15ebf && echo" -alias speedtest="curl -s https://raw.githubusercontent.com/sivel/speedtest-cli/22210ca35228f0bbcef75a7c14587c4ecb875ab4/speedtest.py | python -" - - -# Fix common typos -_command_exists ifconfig && alias ipconfig='ifconfig' -_command_exists R && alias r='R' - - -# Use sane defaults -_command_exists exa && alias exa='exa --group-directories-first --git --time-style=long-iso' -_command_exists netstat && alias ports='netstat -tulanp' -_command_exists screenfetch && alias screenfetch='screenfetch -n' -alias uptime='uptime --pretty' -alias wget='wget --continue' - - -# Create shorter aliases for various utilities -_command_exists batcat && alias bat='batcat' -_command_exists fdfind && alias fd='fdfind' -_command_exists neofetch && alias nf='neofetch' -_command_exists ranger && alias rn='ranger' -_command_exists screenfetch && alias sf='screenfetch' - - -# Integrate git -if _command_exists git; then - alias g='git' - - # All git aliases become shell aliases with a 'g' prefix - for al in $(git internal-aliases); do - # Only "real" (i.e., short) aliases are created - [ ${#al} -lt 7 ] && eval "alias g$al='git $al'" - done - - # Check if a 'main' branch exists in place of a 'master' branch - git_main_branch() { - if [[ -n "$(git branch --list main)" ]]; then - echo 'main' - else - echo 'master' - fi - } -fi diff --git a/.config/shell/init.sh b/.config/shell/init.sh new file mode 100644 index 0000000..f450587 --- /dev/null +++ b/.config/shell/init.sh @@ -0,0 +1,28 @@ +# This file initializes the shell + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + +_init_pyenv () { # used in ~/.config/shell/utils.d/python.sh as well + _command_exists pyenv || return + + eval "$(pyenv init -)" + eval "$(pyenv virtualenv-init -)" +} +_init_pyenv + + +# Configure the keyboard: +# - make right alt and menu keys the compose key, e.g., for German Umlauts +# - make caps lock a ctrl modifier and Esc key +setxkbmap us -option 'compose:menu,compose:ralt,caps:ctrl_modifier' +_command_exists xcape && xcape -e "Caps_Lock=Escape" + + +# Load shell utilities and create aliases +for file in $HOME/.config/shell/{utils.d,aliases.d}/*.sh; do + source $file +done diff --git a/.config/shell/utils.d/README.md b/.config/shell/utils.d/README.md new file mode 100644 index 0000000..abbb008 --- /dev/null +++ b/.config/shell/utils.d/README.md @@ -0,0 +1,7 @@ +# Shell Utilities + +This folder contains various files that provide utilities + to be used in the shell. + +Originally, they all were defined in one big "~/.config/shell/utils.sh" file + but are now split across many smaller files here for clarity. diff --git a/.config/shell/utils.d/files.sh b/.config/shell/utils.d/files.sh new file mode 100644 index 0000000..7bf10f1 --- /dev/null +++ b/.config/shell/utils.d/files.sh @@ -0,0 +1,66 @@ +# The utilities defined here make working with files and folders easier + + +# List the $PATH variable, one element per line +# (if an argument is passed, grep for it) +path() { + if [ -n "$1" ]; then + echo $PATH | perl -p -e 's/:/\n/g;' | grep -i "$1" + else + echo $PATH | perl -p -e 's/:/\n/g;' + fi +} + + +# Show folders by size +disk-usage() { + if [ -n "$1" ]; then + _dest="$1" + else + _dest=. + fi + \du --human-readable --max-depth=1 $_dest 2>/dev/null | sort --human-numeric-sort --reverse +} + + +# Search all files in a directory and its children +lsgrep() { + ls --almost-all --directory . ./**/* | uniq | grep --color=auto -i "$*" +} + +# Make a directory and cd there +mcd() { + test -n "$1" || return + mkdir -p "$1" && cd "$1" || return +} + + +# Extract any compressed archive or file +extract() { + if [ -f "$1" ] ; then + case "$1" in + *.tar.bz2) tar xjvf "$1" ;; + *.tar.gz) tar xzvf "$1" ;; + *.tar.xz) tar xvf "$1" ;; + *.bz2) bzip2 -d "$1" ;; + *.gz) gunzip "$1" ;; + *.tar) tar xf "$1" ;; + *.tbz2) tar xjf "$1" ;; + *.tgz) tar xzf "$1" ;; + *.zip) unzip "$1" ;; + *.Z) uncompress "$1" ;; + *.7z) 7z x "$1" ;; + *) echo "'$1' cannot be extracted automatically" ;; + esac + else + echo "'$1' is not a file" + fi +} + +mktar() { # out of a directory + tar cvzf "${1%%/}.tar.gz" "${1%%/}/" +} + +mkzip() { # out of a file or directory + zip -r "${1%%/}.zip" "$1" +} diff --git a/.config/shell/utils.d/passwords.sh b/.config/shell/utils.d/passwords.sh new file mode 100644 index 0000000..8f089aa --- /dev/null +++ b/.config/shell/utils.d/passwords.sh @@ -0,0 +1,80 @@ +# The utilities defined here allow to create random login credentials + + +# Generate random passwords that are accepted by most services +genpw() { + PARSED=$(getopt --quiet --options=acn: --longoptions=alphanum,clip,chars: -- "$@") + eval set -- "$PARSED" + SYMBOLS='--symbols' + CHARS=30 + XCLIP=false + while true; do + case "$1" in + -a|--alphanum) + SYMBOLS='' + shift + ;; + -c|--clip) + XCLIP=true + shift + ;; + -n|--chars) + CHARS=$2 + shift 2 + ;; + --) + shift + break + ;; + *) + break + ;; + esac + done + PW=$(pwgen --ambiguous --capitalize --numerals --secure $SYMBOLS --remove-chars="|/\\\"\`\'()[]{}<>^~@§$\#" $CHARS 1) + if [[ $XCLIP == true ]]; then + echo $PW | xclip -selection c + else + echo $PW + fi +} + +alias genpw-alphanum='pwgen --ambiguous --capitalize --numerals --secure 30 1' + + +# Random email addresses that look like "normal" ones +genemail() { + PARSED=$(getopt --quiet --options=c --longoptions=clip -- "$@") + eval set -- "$PARSED" + XCLIP=false + while true; do + case "$1" in + -c|--clip) + XCLIP=true + shift + ;; + --) + shift + break + ;; + *) + break + ;; + esac + done + FIRST=$(shuf -i 4-5 -n 1) + LAST=$(shuf -i 8-10 -n 1) + + if _command_exists gpw; then + USER="$(gpw 1 $FIRST).$(gpw 1 $LAST)@webartifex.biz" + else + # Fallback that looks a bit less "normal" + USER="$(pwgen --no-capitalize --no-numerals --secure $FIRST 1).$(pwgen --no-capitalize --no-numerals --secure $LAST 1)@webartifex.biz" + fi + + if [[ $XCLIP == true ]]; then + echo $USER | xclip -selection c + else + echo $USER + fi +} diff --git a/.config/shell/utils.d/python.sh b/.config/shell/utils.d/python.sh new file mode 100644 index 0000000..d63fa6b --- /dev/null +++ b/.config/shell/utils.d/python.sh @@ -0,0 +1,101 @@ +# This file creates a function to install and update the Python develop environments + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + +prepend-to-path () { # if not already there + if [ -d "$1" ] ; then + case :$PATH: in + *:$1:*) ;; + *) PATH=$1:$PATH ;; + esac + fi +} + + + +# The Python versions pyenv creates (in descending order) +# Important: The first version also holds the "interactive" and "utils" environments +_py3_versions=('3.10.6' '3.9.13' '3.8.13' '3.7.13') +_py2_version='2.7.18' + + +# Each Python version receives its own copy of black, pipenv, and poetry +# (e.g., to avoid possible integration problems between pyenv and poetry +# Source: https://github.com/python-poetry/poetry/issues/5252#issuecomment-1055697424) +_py3_site_packages=('black' 'pipenv' 'poetry') + + +# The pyenv virtualenv "utils" contains some globally available tools (e.g., mackup) +_py3_utils=('leglight' 'mackup' 'youtube-dl') + + +# Important: this REMOVES the old ~/.pyenv installation +_install_pyenv() { + echo "(Re-)Installing pyenv" + + # Ensure that pyenv is on the $PATH + # (otherwise, the pyenv installer emits warnings) + mkdir -p "$PYENV_ROOT/bin" + prepend-to-path "$PYENV_ROOT/bin" + + # Remove old pyenv for clean install + rm -rf "$PYENV_ROOT" >/dev/null + + # Run the official pyenv installer + curl https://pyenv.run | bash + + # Make pyenv usable after this installation in the same shell session + _init_pyenv # defined in ~/.config/shell/utils.sh +} + + +create-or-update-python-envs() { + _command_exists pyenv || _install_pyenv + + eval "$(pyenv init --path)" + + # Keep a legacy Python 2.7, just in case + echo "Installing/updating Python $_py2_version" + pyenv install --skip-existing $_py2_version + pyenv rehash # needed on a first install + PYENV_VERSION=$_py2_version pip install --upgrade pip setuptools + PYENV_VERSION=$_py2_version python -c "import sys; print sys.version" + + for version in ${_py3_versions[@]}; do + echo "Installing/updating Python $version" + pyenv install --skip-existing $version + pyenv rehash # needed on a first install + + # Start the new environment with the latest pip and setuptools versions + PYENV_VERSION=$version pip install --upgrade pip setuptools + PYENV_VERSION=$version python -c "import sys; print(sys.version)" + + # Put the specified utilities in the fresh environments or update them + for lib in ${_py3_site_packages[@]}; do + PYENV_VERSION=$version pip install --upgrade $lib + done + done + + # Create a virtualenv based off the latest Python version to host global utilities + echo "Installing/updating the global Python utilities" + pyenv virtualenv $_py3_versions[1] 'utils' + pyenv rehash # needed on a first install + PYENV_VERSION='utils' pip install --upgrade pip setuptools + for util in ${_py3_utils[@]}; do + PYENV_VERSION='utils' pip install --upgrade $util + done + + # Create a virtualenv based off the latest Python version for interactive usage + echo "Installing/updating the default/interactive Python environment" + pyenv virtualenv $_py3_versions[1] 'interactive' + pyenv rehash # needed on a first install + PYENV_VERSION='interactive' pip install --upgrade pip setuptools + # Install some tools to make interactive usage nicer + PYENV_VERSION='interactive' pip install --upgrade black bpython ipython + + # Put all Python binaries/virtualenvs and the utilities on the $PATH + pyenv global 'interactive' $_py3_versions 'utils' $_py2_version +} diff --git a/.config/shell/utils.d/update.sh b/.config/shell/utils.d/update.sh new file mode 100644 index 0000000..3b031fc --- /dev/null +++ b/.config/shell/utils.d/update.sh @@ -0,0 +1,165 @@ +# This file defines the `update-machine` function that updates basically everything + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + +_in_zsh() { + [ -n "$ZSH_VERSION" ] +} + + + +_update_apt() { + _command_exists apt || return + + echo 'Updating apt packages' + sudo apt update + sudo apt upgrade + sudo apt autoremove + sudo apt autoclean +} + +_update_dnf() { + _command_exists dnf || return + + echo 'Updating dnf packages' + sudo dnf upgrade --refresh + sudo dnf autoremove + sudo dnf clean all +} + +_remove_old_snaps() { + sudo snap list --all | awk "/disabled/{print $1, $3}" | + while read snapname revision; do + sudo snap remove "$snapname" --revision="$revision" + done +} + + + +# Update local git repositories (mostly ~/repos) +_update_repositories() { + echo 'Updating repositories' + + cwd=$(pwd) + cd $REPOS + + # Otherwise the for-loop waites for manual input + # if it cd's into a folder with a ".env" file + ZSH_DOTENV_FILE='.do_not_run_dotenv' + + for dir in */; do + cd "$REPOS/$dir" && echo "Fetching $REPOS/$dir" + git fetch --all --prune + done + + ZSH_DOTENV_FILE='.env' + + _command_exists pass && echo "Fetching $HOME/.password-store" && pass git pull + _update_dotfiles + + cd $cwd +} + +# Update the ~/.dotfiles repository +_update_dotfiles() { + echo "Fetching $HOME/.dotfiles" + # The `dotfiles` alias is defined in ~/.bashrc at the end of the + # "Shell Utilities & Aliases" section and can NOT be used here + git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME stash --quiet + git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME fetch --all --prune + git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME pull --rebase --quiet + git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME stash pop # --quiet is ignored +} + + + +_update_zsh() { + _in_zsh || return + + echo 'Updating zsh' + _update_omz_fork + _update_zplug +} + +_update_omz_fork() { + _command_exists omz || return + + # In a nutshell, `omz update` pulls the latest origin/master + # from the original "oh-my-zsh" repo + omz update + + cwd=$(pwd) + cd $ZSH + + git checkout --quiet forked # most likely already the case + + # Keep our personal "oh-my-zsh" fork up-to-date + # See: https://gitlab.webartifex.biz/alexander/oh-my-zsh + git rebase --quiet master + git push --quiet fork forked + git push --quiet fork master + + cd $cwd +} + +_update_zplug() { + _command_exists zplug || return + + zplug update + zplug install # ensure newly added plugins in ~/.zshrc are never forgotten + zplug load +} + + + +_update_python() { + echo 'Updating the Python tool chain' + + if _command_exists pyenv; then + pyenv update + create-or-update-python-envs # defined in ~/.config/shell/utils.d/python.sh + fi + + if _command_exists zsh-pip-cache-packages; then + zsh-pip-clear-cache + zsh-pip-cache-packages + fi +} + + + +_restore_gnome() { + for file in $HOME/.config/gnome-settings/*.ini; do + dconf load / < $file + done +} + + + +run-private-scripts() { # in the Nextcloud + if [ -d "$HOME/data/getraenkemarkt/shell" ]; then + for file in $HOME/data/getraenkemarkt/shell/*.sh; do + source $file + done + fi +} + + + +update-machine() { + sudo --validate || return + + _update_apt + _update_dnf + _command_exists flatpak && sudo flatpak update -y + _command_exists snap && sudo snap refresh && _remove_old_snaps + _update_repositories + _update_zsh + _update_python + _restore_gnome + + sudo --reset-timestamp +} diff --git a/.config/shell/utils.d/web.sh b/.config/shell/utils.d/web.sh new file mode 100644 index 0000000..a58babc --- /dev/null +++ b/.config/shell/utils.d/web.sh @@ -0,0 +1,26 @@ +# This file defines various utilities regarding "the web" + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + +# List all internal IPs +internal-ips() { + if _command_exists ifconfig; then + ifconfig | awk '/inet /{ gsub(/addr:/, ""); print $2 }' + else + echo 'ifconfig not installed' + fi +} + + +# Obtain a weather report +weather() { + if [ -n "$1" ]; then + curl "v1.wttr.in/$1" + else + curl 'v1.wttr.in' + fi +} diff --git a/.config/shell/utils.sh b/.config/shell/utils.sh deleted file mode 100644 index 3429bb0..0000000 --- a/.config/shell/utils.sh +++ /dev/null @@ -1,460 +0,0 @@ -# This file initializes the shell and provides various utility functions - - -_command_exists() { - command -v "$1" 1>/dev/null 2>&1 -} - -_in_zsh() { - [ -n "$ZSH_VERSION" ] -} - -prepend-to-path () { # if not already there - if [ -d "$1" ] ; then - case :$PATH: in - *:$1:*) ;; - *) PATH=$1:$PATH ;; - esac - fi -} - - - -# Configure the keyboard: -# - make right alt and menu keys the compose key, e.g., for umlauts -# - make caps lock a ctrl modifier and Esc key -setxkbmap us -option 'compose:menu,compose:ralt,caps:ctrl_modifier' -_command_exists xcape && xcape -e "Caps_Lock=Escape" - - - -_init_pyenv () { # used further below as well - _command_exists pyenv || return - - eval "$(pyenv init -)" - eval "$(pyenv virtualenv-init -)" -} -_init_pyenv - - - -# ============================== -# Working with files and folders -# ============================== - - -# List the $PATH variable, one element per line -# (if an argument is passed, grep for it) -path() { - if [ -n "$1" ]; then - echo $PATH | perl -p -e 's/:/\n/g;' | grep -i "$1" - else - echo $PATH | perl -p -e 's/:/\n/g;' - fi -} - - -# Show folders by size -disk-usage() { - if [ -n "$1" ]; then - _dest="$1" - else - _dest=. - fi - \du --human-readable --max-depth=1 $_dest 2>/dev/null | sort --human-numeric-sort --reverse -} - - -# Search all files in a directory and its children -lsgrep() { - ls --almost-all --directory . ./**/* | uniq | grep --color=auto -i "$*" -} - -# Make a directory and cd there -mcd() { - test -n "$1" || return - mkdir -p "$1" && cd "$1" || return -} - - -# Extract any compressed archive or file -extract() { - if [ -f "$1" ] ; then - case "$1" in - *.tar.bz2) tar xjvf "$1" ;; - *.tar.gz) tar xzvf "$1" ;; - *.tar.xz) tar xvf "$1" ;; - *.bz2) bzip2 -d "$1" ;; - *.gz) gunzip "$1" ;; - *.tar) tar xf "$1" ;; - *.tbz2) tar xjf "$1" ;; - *.tgz) tar xzf "$1" ;; - *.zip) unzip "$1" ;; - *.Z) uncompress "$1" ;; - *.7z) 7z x "$1" ;; - *) echo "'$1' cannot be extracted automatically" ;; - esac - else - echo "'$1' is not a file" - fi -} - -mktar() { # out of a directory - tar cvzf "${1%%/}.tar.gz" "${1%%/}/" -} - -mkzip() { # out of a file or directory - zip -r "${1%%/}.zip" "$1" -} - - - -# ================================= -# Creating random login credentials -# ================================= - - -genpw() { - PARSED=$(getopt --quiet --options=acn: --longoptions=alphanum,clip,chars: -- "$@") - eval set -- "$PARSED" - SYMBOLS='--symbols' - CHARS=30 - XCLIP=false - while true; do - case "$1" in - -a|--alphanum) - SYMBOLS='' - shift - ;; - -c|--clip) - XCLIP=true - shift - ;; - -n|--chars) - CHARS=$2 - shift 2 - ;; - --) - shift - break - ;; - *) - break - ;; - esac - done - PW=$(pwgen --ambiguous --capitalize --numerals --secure $SYMBOLS --remove-chars="|/\\\"\`\'()[]{}<>^~@§$\#" $CHARS 1) - if [[ $XCLIP == true ]]; then - echo $PW | xclip -selection c - else - echo $PW - fi -} - -alias genpw-alphanum='pwgen --ambiguous --capitalize --numerals --secure 30 1' - - -# Random email addresses that look like "normal" ones -genemail() { - PARSED=$(getopt --quiet --options=c --longoptions=clip -- "$@") - eval set -- "$PARSED" - XCLIP=false - while true; do - case "$1" in - -c|--clip) - XCLIP=true - shift - ;; - --) - shift - break - ;; - *) - break - ;; - esac - done - FIRST=$(shuf -i 4-5 -n 1) - LAST=$(shuf -i 8-10 -n 1) - - if _command_exists gpw; then - USER="$(gpw 1 $FIRST).$(gpw 1 $LAST)@webartifex.biz" - else - # Fallback that looks a bit less "normal" - USER="$(pwgen --no-capitalize --no-numerals --secure $FIRST 1).$(pwgen --no-capitalize --no-numerals --secure $LAST 1)@webartifex.biz" - fi - - if [[ $XCLIP == true ]]; then - echo $USER | xclip -selection c - else - echo $USER - fi -} - - - -# =================================================== -# Set up & maintain the Python (develop) environments -# =================================================== - - -# TODO: This needs to be updated regularly (or find an automated solution) -# The Python versions pyenv creates (in descending order -# Important: The first version also holds the "interactive" and "utils" environments) -_py3_versions=('3.10.6' '3.9.13' '3.8.13' '3.7.13') -_py2_version='2.7.18' - -# Each Python version receives its own copy of black, pipenv, and poetry -# (e.g., to avoid possible integration problems between pyenv and poetry -# Source: https://github.com/python-poetry/poetry/issues/5252#issuecomment-1055697424) -_py3_site_packages=('black' 'pipenv' 'poetry') - -# The pyenv virtualenv "utils" contains some globally available tools (e.g., mackup) -_py3_utils=('leglight' 'mackup' 'youtube-dl') - -# Important: this REMOVES the old ~/.pyenv installation -_install_pyenv() { - echo "(Re-)Installing pyenv" - - # Ensure that pyenv is on the $PATH - # (otherwise, the pyenv installer emits warnings) - mkdir -p "$PYENV_ROOT/bin" - prepend-to-path "$PYENV_ROOT/bin" - - # Remove old pyenv for clean install - rm -rf "$PYENV_ROOT" >/dev/null - - # Run the official pyenv installer - curl https://pyenv.run | bash - - # Make pyenv usable after this installation in the same shell session - _init_pyenv -} - -create-or-update-python-envs() { - _command_exists pyenv || _install_pyenv - - eval "$(pyenv init --path)" - - # Keep a legacy Python 2.7, just in case - echo "Installing/updating Python $_py2_version" - pyenv install --skip-existing $_py2_version - pyenv rehash # needed on a first install - PYENV_VERSION=$_py2_version pip install --upgrade pip setuptools - PYENV_VERSION=$_py2_version python -c "import sys; print sys.version" - - for version in ${_py3_versions[@]}; do - echo "Installing/updating Python $version" - pyenv install --skip-existing $version - pyenv rehash # needed on a first install - - # Start the new environment with the latest pip and setuptools versions - PYENV_VERSION=$version pip install --upgrade pip setuptools - PYENV_VERSION=$version python -c "import sys; print(sys.version)" - - # Put the specified utilities in the fresh environments or update them - for lib in ${_py3_site_packages[@]}; do - PYENV_VERSION=$version pip install --upgrade $lib - done - done - - # Create a virtualenv based off the latest Python version to host global utilities - echo "Installing/updating the global Python utilities" - pyenv virtualenv $_py3_versions[1] 'utils' - pyenv rehash # needed on a first install - PYENV_VERSION='utils' pip install --upgrade pip setuptools - for util in ${_py3_utils[@]}; do - PYENV_VERSION='utils' pip install --upgrade $util - done - - # Create a virtualenv based off the latest Python version for interactive usage - echo "Installing/updating the default/interactive Python environment" - pyenv virtualenv $_py3_versions[1] 'interactive' - pyenv rehash # needed on a first install - PYENV_VERSION='interactive' pip install --upgrade pip setuptools - # Install some tools to make interactive usage nicer - PYENV_VERSION='interactive' pip install --upgrade black bpython ipython - - # Put all Python binaries/virtualenvs and the utilities on the $PATH - pyenv global 'interactive' $_py3_versions 'utils' $_py2_version -} - - - -# ============================= -# Automate the update machinery -# ============================= - - -_update_apt() { - _command_exists apt || return - - echo 'Updating apt packages' - sudo apt update - sudo apt upgrade - sudo apt autoremove - sudo apt autoclean -} - -_update_dnf() { - _command_exists dnf || return - - echo 'Updating dnf packages' - sudo dnf upgrade --refresh - sudo dnf autoremove - sudo dnf clean all -} - -_remove_old_snaps() { - sudo snap list --all | awk "/disabled/{print $1, $3}" | - while read snapname revision; do - sudo snap remove "$snapname" --revision="$revision" - done -} - - -# Update local git repositories (mostly ~/repos) -_update_repositories() { - echo 'Updating repositories' - - cwd=$(pwd) - cd $REPOS - - # Otherwise the for-loop waites for manual input - # if it cd's into a folder with a ".env" file - ZSH_DOTENV_FILE='.do_not_run_dotenv' - - for dir in */; do - cd "$REPOS/$dir" && echo "Fetching $REPOS/$dir" - git fetch --all --prune - done - - ZSH_DOTENV_FILE='.env' - - _command_exists pass && echo "Fetching $HOME/.password-store" && pass git pull - _update_dotfiles - - cd $cwd -} - -# Update the ~/.dotfiles repository -_update_dotfiles() { - echo "Fetching $HOME/.dotfiles" - # The `dotfiles` alias is defined in ~/.bashrc at the end of the - # "Shell Utilities & Aliases" section and can NOT be used here - git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME stash --quiet - git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME fetch --all --prune - git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME pull --rebase --quiet - git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME stash pop # --quiet is ignored -} - - -_update_zsh() { - _in_zsh || return - - echo 'Updating zsh' - _update_omz_fork - _update_zplug -} - -_update_omz_fork() { - _command_exists omz || return - - # In a nutshell, `omz update` pulls the latest origin/master - # from the original "oh-my-zsh" repo - omz update - - cwd=$(pwd) - cd $ZSH - - git checkout --quiet forked # most likely already the case - - # Keep our personal "oh-my-zsh" fork up-to-date - # See: https://gitlab.webartifex.biz/alexander/oh-my-zsh - git rebase --quiet master - git push --quiet fork forked - git push --quiet fork master - - cd $cwd -} - -_update_zplug() { - _command_exists zplug || return - - zplug update - zplug install # ensure newly added plugins in ~/.zshrc are never forgotten - zplug load -} - - -_update_python() { - echo 'Updating the Python tool chain' - - if _command_exists pyenv; then - pyenv update - create-or-update-python-envs - fi - - if _command_exists zsh-pip-cache-packages; then - zsh-pip-clear-cache - zsh-pip-cache-packages - fi -} - - -_restore_gnome() { - for file in $HOME/.config/gnome-settings/*.ini; do - dconf load / < $file - done -} - - -run-private-scripts() { # in the Nextcloud - if [ -d "$HOME/data/getraenkemarkt/shell" ]; then - for file in $HOME/data/getraenkemarkt/shell/*.sh; do - source $file - done - fi -} - - -update-machine() { - sudo --validate || return - - _update_apt - _update_dnf - _command_exists flatpak && sudo flatpak update -y - _command_exists snap && sudo snap refresh && _remove_old_snaps - _update_repositories - _update_zsh - _update_python - _restore_gnome - - sudo --reset-timestamp -} - - - -# ======================= -# Various other Utilities -# ======================= - - -# List all internal IPs -internal-ips() { - if _command_exists ifconfig; then - ifconfig | awk '/inet /{ gsub(/addr:/, ""); print $2 }' - else - echo 'ifconfig not installed' - fi -} - - -# Obtain a weather report -weather() { - if [ -n "$1" ]; then - curl "v1.wttr.in/$1" - else - curl 'v1.wttr.in' - fi -} diff --git a/.zshrc b/.zshrc index 1a9992d..0d4a86e 100644 --- a/.zshrc +++ b/.zshrc @@ -102,8 +102,7 @@ zplug load # Initialize various utilities and aliases -source "$HOME/.config/shell/utils.sh" -source "$HOME/.config/shell/aliases.sh" +source "$HOME/.config/shell/init.sh"