diff --git a/.bash_login b/.bash_login new file mode 100644 index 0000000..168e140 --- /dev/null +++ b/.bash_login @@ -0,0 +1,8 @@ +# Executed by bash when a login shell starts + +# Mimic bash's default behavior explicitly +if [ -f "$HOME/.profile" ]; then + source "$HOME/.profile" +else + source "$HOME/.bashrc" +fi diff --git a/.bash_logout b/.bash_logout new file mode 100644 index 0000000..b80458c --- /dev/null +++ b/.bash_logout @@ -0,0 +1,6 @@ +# Executed by bash when a login shell exits + +# Clear the screen to increase privacy +if [ "$SHLVL" = 1 ]; then + [ -x /usr/bin/clear ] && /usr/bin/clear || [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q +fi diff --git a/.bash_profile b/.bash_profile new file mode 100644 index 0000000..106a7a9 --- /dev/null +++ b/.bash_profile @@ -0,0 +1,10 @@ +# Executed by bash when a login shell starts + +# Mimic bash's default behavior explicitly +if [ -f "$HOME/.bash_login" ]; then + source "$HOME/.bash_login" +elif [ -f "$HOME/.profile" ]; then + source "$HOME/.profile" +else + source "$HOME/.bashrc" +fi diff --git a/.bashrc b/.bashrc new file mode 100644 index 0000000..c967a32 --- /dev/null +++ b/.bashrc @@ -0,0 +1,197 @@ +# Executed by bash when a (non-)login shell starts + + +# Ensure bash is running interactively +[[ $- != *i* ]] && return + + + +# Enable XON/XOFF software flow control +stty -ixon + +# Report status of background jobs immediately +set -o notify +# Show number of running jobs when exiting a shell +shopt -s checkjobs + +# Just type the directory to cd into it +shopt -s autocd +# Correct minor spelling mistakes with cd +shopt -s cdspell + +# Include hidden files in * glob expansion +shopt -s dotglob +shopt -s extglob +# Expand ** into (recursive) directories +shopt -s globstar +# Ignore case when * expanding +shopt -s nocaseglob + +# Update $ROWS and $COLUMNS after each command +shopt -s checkwinsize + + + +# Set these environment variables here (and not in ~/.profile) +# due to conflict/overlap with zsh +export HISTFILE="$HOME/.bash_history" +export HISTSIZE=999999 # number of lines kept in memory +export HISTFILESIZE=999999 # number of lines kept in $HISTFILE + +# Ignore commands prefixed with a space, +# and ones entered identically just before +# (this mimics zsh's default behavior) +export HISTCONTROL=ignoreboth + +# Remember multi-line commands in history as one command +shopt -s cmdhist +# Do not overwrite .bash_history file +shopt -s histappend +# Allow re-editing a failed history substitution +shopt -s histreedit +# Store multi-line commands in history without semicolons +shopt -s lithist + + + +# Initialize various utilities and aliases +source "$HOME/.config/shell/utils.sh" +source "$HOME/.config/shell/aliases.sh" + + + +# Enable programmable completion features +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + source /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + source /etc/bash_completion + fi +fi + +# Add tab completion for all aliases to commands with completion functions +# (must come after bash completions have been set up) +# Source: https://superuser.com/a/437508 +_alias_completion() { + local namespace="alias_completion" + # parse function based completion definitions, where capture group 2 => function and 3 => trigger + local compl_regex='complete( +[^ ]+)* -F ([^ ]+) ("[^"]+"|[^ ]+)' + # parse alias definitions, where capture group 1 => trigger, 2 => command, 3 => command arguments + local alias_regex="alias ([^=]+)='(\"[^\"]+\"|[^ ]+)(( +[^ ]+)*)'" + # create array of function completion triggers, keeping multi-word triggers together + eval "local completions=($(complete -p | sed -Ene "/$compl_regex/s//'\3'/p"))" + (( ${#completions[@]} == 0 )) && return 0 + # create temporary file for wrapper functions and completions + rm -f "/tmp/${namespace}-*.tmp" # preliminary cleanup + local tmp_file; tmp_file="$(mktemp "/tmp/${namespace}-${RANDOM}XXX.tmp")" || return 1 + local completion_loader; completion_loader="$(complete -p -D 2>/dev/null | sed -Ene 's/.* -F ([^ ]*).*/\1/p')" + # read in " '' ''" lines from defined aliases + local line; while read line; do + eval "local alias_tokens; alias_tokens=($line)" 2>/dev/null || continue # some alias arg patterns cause an eval parse error + local alias_name="${alias_tokens[0]}" alias_cmd="${alias_tokens[1]}" alias_args="${alias_tokens[2]# }" + # skip aliases to pipes, boolean control structures and other command lists + # (leveraging that eval errs out if $alias_args contains unquoted shell metacharacters) + eval "local alias_arg_words; alias_arg_words=($alias_args)" 2>/dev/null || continue + # avoid expanding wildcards + read -a alias_arg_words <<< "$alias_args" + # skip alias if there is no completion function triggered by the aliased command + if [[ ! " ${completions[*]} " =~ " $alias_cmd " ]]; then + if [[ -n "$completion_loader" ]]; then + # force loading of completions for the aliased command + eval "$completion_loader $alias_cmd" + # 124 means completion loader was successful + [[ $? -eq 124 ]] || continue + completions+=($alias_cmd) + else + continue + fi + fi + local new_completion="$(complete -p "$alias_cmd")" + # create a wrapper inserting the alias arguments if any + if [[ -n $alias_args ]]; then + local compl_func="${new_completion/#* -F /}"; compl_func="${compl_func%% *}" + # avoid recursive call loops by ignoring our own functions + if [[ "${compl_func#_$namespace::}" == $compl_func ]]; then + local compl_wrapper="_${namespace}::${alias_name}" + echo "function $compl_wrapper { + (( COMP_CWORD += ${#alias_arg_words[@]} )) + COMP_WORDS=($alias_cmd $alias_args \${COMP_WORDS[@]:1}) + (( COMP_POINT -= \${#COMP_LINE} )) + COMP_LINE=\${COMP_LINE/$alias_name/$alias_cmd $alias_args} + (( COMP_POINT += \${#COMP_LINE} )) + $compl_func + }" >> "$tmp_file" + new_completion="${new_completion/ -F $compl_func / -F $compl_wrapper }" + fi + fi + # replace completion trigger by alias + new_completion="${new_completion% *} $alias_name" + echo "$new_completion" >> "$tmp_file" + done < <(alias -p | sed -Ene "s/$alias_regex/\1 '\2' '\3'/p") + source "$tmp_file" && \rm -f "$tmp_file" +}; _alias_completion + + + +# Mimic zsh's PowerLevel10k + +if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then + _debian_chroot=$(cat /etc/debian_chroot) +fi + +_prompt_git() { # Show a git repo's state + local out ref uncommited unstaged untracked ahead behind + # Check if the pwd contains a git repository and exit early if it does not + ref=$(git rev-parse --abbrev-ref --symbolic-full-name HEAD 2> /dev/null) + [ "$ref" == "" ] && return + # Check if the current HEAD is detached or reachable by a ref + printf "\033[0;37m " + if [ "$ref" == "HEAD" ]; then + ref=$(git rev-parse --short HEAD) + printf "@" + fi + printf "\033[0;32m$ref\033[0m" + # Indicate if local is ahead and/or behind upstream + ahead=0 + behind=0 + git status 2>/dev/null | ( + while read -r line ; do + case "$line" in + *'diverged'*) # For simplicity, a diverged local branch is + ahead=1 ; behind=1 ; break ; ;; # indicated as being + *'ahead'*) # both ahead and behind its upstream + ahead=1 ; ;; + *'behind'*) + behind=1 ; ;; + esac + done + + if [ $ahead -gt 0 ] && [ $behind -gt 0 ]; then + printf "\033[0;32m <>\033[0m" + elif [ $ahead -gt 0 ]; then + printf "\033[0;32m >\033[0m" + elif [ $behind -gt 0 ]; then + printf "\033[0;32m <\033[0m" + fi + ) + # Indicate stashed files with a * + [ "$(git stash list 2> /dev/null)" != "" ] && printf "\033[0;32m *\033[0m" + # Indicate uncommited/staged with a + + git diff-index --cached --exit-code --quiet HEAD -- 2> /dev/null + [ $? -gt 0 ] && printf "\033[0;33m +\033[0m" + # Indicate unstaged with a ! + git diff-files --exit-code --quiet 2> /dev/null + [ $? -gt 0 ] && printf "\033[0;33m !\033[0m" + # Indicate untracked files with a ? + if [ "$(git ls-files --exclude-standard --others 2> /dev/null)" != "" ]; then + printf "\033[0;34m ?\033[0m" + fi +} + +_prompt_jobs() { # Indicate running background jobs with a"%" + local running + (( $(jobs -rp | wc -l) )) && printf "\033[0;32m %\033[0m" +} + +PS1='${chroot:+($_debian_chroot)}\w$(_prompt_git)$(_prompt_jobs) > ' +PS2='... ' diff --git a/.config/shell/README.md b/.config/shell/README.md new file mode 100644 index 0000000..7aa8eda --- /dev/null +++ b/.config/shell/README.md @@ -0,0 +1,3 @@ +# Shell-related Configuration + +This folder contains further files that are sourced by `bash`. diff --git a/.config/shell/aliases.sh b/.config/shell/aliases.sh new file mode 100644 index 0000000..d4aaf3c --- /dev/null +++ b/.config/shell/aliases.sh @@ -0,0 +1,97 @@ +# Shell aliases for bash + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + + +# Re-run last command with sudo privileges +alias ,,='sudo $(history -p !!)' + + +# (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' + + +# 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/utils.sh b/.config/shell/utils.sh new file mode 100644 index 0000000..9c76cb0 --- /dev/null +++ b/.config/shell/utils.sh @@ -0,0 +1,271 @@ +# This file initializes the shell and provides various utility functions + + +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + + +# 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" + + + +# ============================== +# 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 +} + + + +# ============================= +# 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 + + for dir in */; do + cd "$REPOS/$dir" && echo "Fetching $REPOS/$dir" + git fetch --all --prune + done + + _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-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 + + 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/.profile b/.profile new file mode 100644 index 0000000..1eb7c23 --- /dev/null +++ b/.profile @@ -0,0 +1,36 @@ +# Executed by a login shell (e.g., bash, sh, or zsh) during start-up + + +export PAGER='less --chop-long-lines --ignore-case --LONG-PROMPT --no-init --status-column --quit-if-one-screen' +export TERM=xterm-256color +export TZ='Europe/Berlin' + +export REPOS="$HOME/repos" + +export LESSHISTFILE="$HOME/.lesshst" + + + +prepend-to-path () { # if not already there + if [ -d "$1" ] ; then + case :$PATH: in + *:$1:*) ;; + *) PATH=$1:$PATH ;; + esac + fi +} + +prepend-to-path "$HOME/bin" +prepend-to-path "$HOME/.local/bin" + + + +# Shell-specific stuff + +# Source ~/.bashrc if we are running inside a bash shell +# because it is NOT automatically sourced by bash +if [ -n "$BASH_VERSION" ]; then + if [ -f "$HOME/.bashrc" ]; then + source "$HOME/.bashrc" + fi +fi