Skip to content

Instantly share code, notes, and snippets.

@Raiu
Last active January 15, 2025 21:14
Show Gist options
  • Save Raiu/579a993b9c5dcda726bf561ffc9df3f4 to your computer and use it in GitHub Desktop.
Save Raiu/579a993b9c5dcda726bf561ffc9df3f4 to your computer and use it in GitHub Desktop.
#!/bin/sh
#
# Bash and Vim Environment Setup Script
# =====================================
#
# Purpose:
# Automates the setup of a Bash environment with practical defaults, adhering
# to XDG Base Directory standards, ensuring clean, isolated, and maintainable configurations.
#
# Features:
# - Configures Bash with custom `.bashrc` and `.profile`.
# - Sets up Vim with vim-plug for modern plugin management.
# - Supports quiet mode (-q) for automated scripts.
# - Includes cleanup option to remove old configurations and history files.
# - Fully POSIX-compliant for compatibility with minimal environments.
#
# Usage:
# sh -c "$(curl -fsSL https://gist.githubusercontent.com/Raiu/579a993b9c5dcda726bf561ffc9df3f4/raw/setup_bash.sh)"
# sh -c "$(curl -fsSL https://gist.githubusercontent.com/Raiu/579a993b9c5dcda726bf561ffc9df3f4/raw/setup_bash.sh)" -- -q
# sh -c "$(curl -fsSL https://gist.githubusercontent.com/Raiu/579a993b9c5dcda726bf561ffc9df3f4/raw/setup_bash.sh)" -- -q --clean
# sh -c "$(curl -fsSL https://gist.githubusercontent.com/Raiu/579a993b9c5dcda726bf561ffc9df3f4/raw/setup_bash.sh)" -- --clean
#
# Other useful packages:
# bat, fzf,
# Eza:
# curl -sL https://github.com/eza-community/eza/releases/latest/download/eza_x86_64-unknown-linux-gnu.tar.gz | tar xz -C /tmp && sudo mv /tmp/eza /usr/local/bin/eza
#
set -e
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
BASH_DIR="${XDG_CONFIG_HOME}/bash"
PROFILE="${BASH_DIR}/profile"
BASHRC="${BASH_DIR}/bashrc"
VIM_DIR="${XDG_CONFIG_HOME}/vim"
VIMRC="${VIM_DIR}/vimrc"
VIMRC_MIN="${VIM_DIR}/vimrc.minimal"
CLEANUP_FILES=".bash_history:.bashrc:.bash_profile:.inputrc:.vimrc:.viminfo:.wget-hsts:.lesshst:.sudo_as_admin_successful"
log() { [ "$QUIET" -ne 1 ] && echo "$@"; }
error() { echo "Error: $*" >&2; exit 1; }
exist() { command -v "$1" >/dev/null 2>&1; }
BASHRC_CONTENT=$(
cat <<'EOF'
# Helpers
_exist() { command -v "$1" >/dev/null 2>&1; }
# Blues: Light (BL1), Medium (BL2), Sky (BL3)
BL1="\[$(tput setaf 12)\]" BL2="\[$(tput setaf 45)\]" BL3="\[$(tput setaf 51)\]"
# Reds: Bright (RD1), Crimson (RD2), Intense (RD3)
RD1="\[$(tput setaf 9)\]" RD2="\[$(tput setaf 160)\]" RD3="\[$(tput setaf 196)\]"
# Greens: Light (GR1), Medium (GR2), Deep (GR3)
GR1="\[$(tput setaf 10)\]" GR2="\[$(tput setaf 40)\]" GR3="\[$(tput setaf 46)\]"
# Yellows/Oranges: Bright Yellow (YL1), Intense Yellow (YL2), Light Orange (OR1), Dark Orange (OR2)
YL1="\[$(tput setaf 11)\]" YL2="\[$(tput setaf 226)\]" OR1="\[$(tput setaf 208)\]" OR2="\[$(tput setaf 202)\]"
# Purples: Bright (PR1), Deep (PR2), Vibrant (PR3)
PR1="\[$(tput setaf 13)\]" PR2="\[$(tput setaf 165)\]" PR3="\[$(tput setaf 201)\]"
# White/Black: Bright White (WH1), Light White (WH2), Light Gray (GY1), Dark Gray (GY2), Black (BK1)
WH1="\[$(tput setaf 231)\]" WH2="\[$(tput setaf 15)\]" GY1="\[$(tput setaf 250)\]" GY2="\[$(tput setaf 240)\]" BK1="\[$(tput setaf 232)\]"
# Reset
RES="\[$(tput sgr0)\]"
# Set XDG Base Directories
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
mkdir -p "${XDG_CACHE_HOME}/bash" "${XDG_DATA_HOME}/bash" "${XDG_STATE_HOME}/bash" || {
echo "Failed to create directories" >&2
}
# Other exports
export FZF_DEFAULT_OPTS="--height 40% --layout=reverse --border"
# General Environment Settings
umask 022
shopt -s checkwinsize
shopt -s autocd
shopt -s globstar
# History Configuration
HISTFILE="${XDG_STATE_HOME}/bash/history"
[ ! -f "$HISTFILE" ] && touch "$HISTFILE"
HISTSIZE=1000
HISTFILESIZE=2000
shopt -s histappend
export HISTCONTROL=ignoredups:erasedups
# Enable Completion
if [ -f "$XDG_CONFIG_HOME/bash_completion" ]; then
. "$XDG_CONFIG_HOME/bash_completion"
elif [ -f "/usr/share/bash-completion/bash_completion" ]; then
. "/usr/share/bash-completion/bash_completion"
fi
if _exist fzf; then
fzf_kbpath=$(find /usr/share -type f -name "key-bindings.bash" 2>/dev/null | head -n 1)
[ -f "$fzf_kbpath" ] && source "$fzf_kbpath"
fi
# Binds
if [ "$PS1" ] ; then
bind '"\C-@": " \C-u$(fzf)\e\C-e\er"' # open fzf with ctrl-space
bind 'set show-all-if-ambiguous on'
bind 'TAB:menu-complete'
fi
# Prompt
LAST_DIR=""
chpwd() { [[ "$LAST_DIR" != "$PWD" ]] && LAST_DIR="$PWD" && ls; }
prompt() {
PS1="\n${OR1}\u${RES}@${BL1}\h${RES}: ${BL3}\w${RES}\n"
[[ $_ecode -eq 0 ]] && PS1+="${GR3}→${RES} " || PS1+="${RD3}→${RES} "
}
export PROMPT_COMMAND='_ecode=$?; history -a; history -c; history -r; chpwd; prompt'
# Functions
batman() {
_exist bat || { echo "Error: 'bat' is not installed." >&2; return 1; }
[[ -z "$1" ]] && { echo "Usage: batman <command>" >&2; return 1; }
man "$1" | col -bx | bat --paging=always --language=man
}
# Alias Definitions
_exist "batcat" && alias bat="batcat"
_exist "fdfind" && alias fd="fdfind"
if _exist "eza"; then
alias ls='eza --icons --group-directories-first'
alias lsa='eza -a --icons --group-directories-first'
alias lt='eza -T --group-directories-first --icons --git'
alias lta='eza -Ta --group-directories-first --icons --git'
alias ll='eza -lmh --group-directories-first --color-scale --icons'
alias la='eza -lamhg --group-directories-first --color-scale --icons --git'
alias laa='eza -lamhg@ --group-directories-first --color-scale --icons --git'
alias lx='eza -lbhHigUmuSa@ --group-directories-first --color-scale --icons --git --time-style=long-iso'
else
alias ls='ls -h --group-directories-first --color=auto'
alias lsa='ls -Ah --group-directories-first --color=auto'
alias ll='ls -lh --group-directories-first --color=auto'
alias la='ls -lAh --group-directories-first --color=auto'
fi
alias c='clear'
alias h='history'
alias ..='cd ..'
alias ...='cd ../..'
alias md='mkdir -p'
alias rd='rmdir'
alias egrep='grep -E --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox}'
alias fgrep='grep -F --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox}'
alias wget='wget --no-hsts'
alias bed='vim ${XDG_CONFIG_HOME}/bash/bashrc'
alias bre='source ${XDG_CONFIG_HOME}/bash/bashrc'
alias ved='vim ${XDG_CONFIG_HOME}/vim/vimrc'
alias vim.recover='vim -u ${XDG_CONFIG_HOME}/vim/vimrc.minimal -U NONE -i NONE ${XDG_CONFIG_HOME}/vim/vimrc'
alias minvim='vim -u ${XDG_CONFIG_HOME}/vim/vimrc.minimal -U NONE -i NONE'
EOF
)
PROFILE_CONTENT=$(
cat <<'EOF'
# XDG Base Directory Specification
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
# Ensure XDG directories exist
mkdir -p "${XDG_CONFIG_HOME}/bash" "${XDG_DATA_HOME}/bash"\
"${XDG_CACHE_HOME}/bash" "${XDG_STATE_HOME}/bash"
# Ensure PATH includes common directories
old_path=$PATH:"$HOME/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
new_path=""
for dir in $(echo "$old_path" | tr ':' '\n' | sed '1!G;h;$!d'); do
case ":$new_path:" in
*":$dir:"*) ;;
*) new_path="$dir${new_path:+:$new_path}" ;;
esac
done
export PATH="$new_path"
# Source interactive shell configurations only if the shell is Bash and interactive
if [ -n "$BASH" ] && [ "$-" != "${-#*i}" ]; then
if [ -f "$XDG_CONFIG_HOME/bash/bashrc" ]; then
. "$XDG_CONFIG_HOME/bash/bashrc"
elif [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
# VIM
export VIMINIT='let $MYVIMRC = $XDG_CONFIG_HOME . "/vim/vimrc" | source $MYVIMRC'
EOF
)
VIMRC_CONTENT=$(
cat <<'EOF'
" ====== XDG Compliance ======
let $XDG_CONFIG_HOME = exists('$XDG_CONFIG_HOME') ? $XDG_CONFIG_HOME : expand('~/.config')
let $XDG_DATA_HOME = exists('$XDG_DATA_HOME') ? $XDG_DATA_HOME : expand('~/.local/share')
let $VIMDOTDIR = $XDG_CONFIG_HOME . '/vim'
let $VIMDATADIR = $XDG_DATA_HOME . '/vim'
" ====== General Settings ======
syntax on " Enable syntax highlighting
filetype plugin indent on " Enable filetype detection and indentation
set clipboard=unnamedplus " Use system clipboard
set hlsearch " Highlight search results
set ignorecase smartcase " Case-insensitive search unless uppercase is used
set incsearch " Incremental search
set nocursorline " Highlight the current line
set number relativenumber " Show line numbers
set mouse=a " Enable mouse support
set scrolloff=5 " Keep 5 lines visible when scrolling
set showmode " Display mode in status line
set sidescrolloff=5 " Horizontal scrolling offset
set splitbelow splitright " Open splits to the right and below
set tabstop=4 shiftwidth=4 expandtab " Use spaces instead of tabs
set timeoutlen=500 " 500ms for mappings
set ttimeoutlen=10 " Faster key timeout
set updatetime=300 " Faster updates for a smoother experience
" ====== Directories and Files ======
silent! call mkdir($VIMDATADIR . '/backup', 'p')
silent! call mkdir($VIMDATADIR . '/undo', 'p')
silent! call mkdir($VIMDATADIR . '/swap', 'p')
set backupdir=$VIMDATADIR/backup//
set undodir=$VIMDATADIR/undo//
set directory=$VIMDATADIR/swap//
set backup
set undofile
set viminfo+=n$VIMDATADIR/viminfo
" ====== Colorscheme ======
colorscheme slate
set background=dark
" ====== Statusline ======
set statusline=%<%f\ %h%m%r\ %y\ [%{&fileformat}]\ [%{&fileencoding}]\ %=%-14.(%l,%c%V%)\ %P
set laststatus=2
" ====== Mode Settings ======
" Insert mode cursorline
au InsertEnter * set cul
au InsertLeave * set nocul
" Insert mode lin shape
let &t_SI = "\e[6 q"
let &t_EI = "\e[2 q"
" ====== Key Mappings ======
let mapleader = ","
" Edit and Source vimrc
nnoremap <leader>ev :e $MYVIMRC<CR>
nnoremap <leader>sv :source $MYVIMRC<CR>
" Toggle between Normal and Insert mode
nnoremap <C-@> a
inoremap <C-@> <Esc>
vnoremap <C-@> <Esc>
" Easy file saving and quitting
nnoremap <leader>w :w<CR>
nnoremap <leader>q :q<CR>
" Save all files and Quit all files
nnoremap <leader>W :wa<CR>
nnoremap <leader>Q :qa<CR>
" Clear search highlights
nnoremap <leader>h :nohlsearch<CR>
" Window navigation
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l
" Split management
nnoremap <leader>v :vsplit<CR>
nnoremap <leader>s :split<CR>
" Quick search and replace
nnoremap <leader>r :%s//g<Left><Left>
" Yank selected text into register (z)
vnoremap <leader>y "zy
" Search-replace using text from register (z)
nnoremap <leader>c :<C-u>let @/ = escape(@z, '/\\')<CR>:%s/<C-r>=@z<CR>/<C-r>=@z<CR>/g<Left><Left>
vnoremap <leader>c :<C-u>let @/ = escape(@z, '/\\')<CR>:'<,'>s/<C-r>=@z<CR>/<C-r>=@z<CR>/g<Left><Left>
" Search-replace using selection as search pattern
vnoremap <leader>r "hy:let @/ = escape(@h, '/\\.*^$[]~')<CR>:%s/<C-r>=@/<CR>/<C-r>=@/<CR>/g<Left><Left>
" edit or source vimrc
nnoremap <Leader>e :edit $MYVIMRC<CR>
" vv for Visual Character mode
nnoremap vv v
vnoremap vv v
" vb for Visual Block mode
nnoremap vb <C-v>
vnoremap vb <C-v>
" Manual trimming for all file types
nnoremap <leader>t :silent! %s/\s\+$//e<CR>:echo "Trailing whitespace removed!"<CR>
" Toggle search hl
nnoremap <F2> :set hlsearch!<CR>:echo "Search hl " . (&hlsearch ? "enabled" : "disabled")<CR>
" Show list of open buffers
nnoremap <F3> :ls<CR>:b
" Toggle file explorer
nnoremap <F4> :Lexplore<CR>
" Cycle buffers (silent)
nnoremap <F5> :silent! bnext<CR>:echo "Next buffer: " . bufname('%')<CR>
nnoremap <F6> :silent! bprev<CR>:echo "Prev buffer: " . bufname('%')<CR>
" ====== Functions ======
" ====== Autocommands ======
if has("autocmd")
" Automatically source vimrc on save
autocmd! BufWritePost $MYVIMRC silent! source $MYVIMRC | redraw! | echo "vimrc sourced"
" Automatically reload files when changed externally
autocmd FocusGained,BufEnter * checktime
" Highlight yanked text
autocmd TextYankPost * silent! lua vim.highlight.on_yank {higroup="IncSearch", timeout=300}
" Automatically create parent directories on save
autocmd BufWritePre * call mkdir(expand('<afile>:p:h'), 'p')
" Trim trailing whitespace on save for specific file types
autocmd BufWritePre *.c,*.cpp,*.h,*.py,*.js,*.ts,*.html,*.css,*.json,*.yml,*.yaml,*.sql,*.sh,*.go %s/\s\+$//e
endif
" ====== Miscellaneous ======
EOF
)
VIMRC_MIN_CONTENT=$(
cat <<'EOF'
filetype plugin indent on
syntax on
set backspace=indent,eol,start
set hlsearch
set ignorecase smartcase
set nocompatible
set noerrorbells
set nowrap
set number
set ruler
set tabstop=4 shiftwidth=4 expandtab
EOF
)
symlink() {
[ ! -d "$(dirname "$2")" ] && mkdir -p "$(dirname "$2")"
[ -L "$2" ] && [ "$(readlink -f "$2")" = "$(readlink -f "$1")" ] && return 0
[ -e "$2" ] && [ ! -L "$2" ] && mv "$2" "$2.bak"
ln -sf "$1" "$2"
}
download_file() {
if exist curl; then
curl -sSfLo "$2" "$1" || error "Failed to download $1 with curl."
elif exist wget; then
wget -qO "$2" "$1" || error "Failed to download $1 with wget."
else
error "curl or wget is required."
fi
}
create_xdg_dirs() {
mkdir -p "${BASH_DIR}" "${XDG_CACHE_HOME}/bash" "${XDG_DATA_HOME}/bash" \
"${VIM_DIR}" "${XDG_CACHE_HOME}/vim" "${XDG_DATA_HOME}/vim"
}
deploy_bash() {
echo "${BASHRC_CONTENT}" >"${BASHRC}"
echo "${PROFILE_CONTENT}" >"${PROFILE}"
symlink "${PROFILE}" "$HOME/.profile"
}
deploy_vim() {
echo "${VIMRC_CONTENT}" >"${VIMRC}"
echo "${VIMRC_MIN_CONTENT}" >"${VIMRC_MIN}"
}
check_dependencies() {
exist bash || error "bash is required but not installed."
exist vim || error "vim is required but not installed."
exist curl || exist wget || error "curl or wget is required but neither is installed."
}
cleanup() {
log "Cleaning up old configurations and history files..."
echo "$CLEANUP_FILES" | tr ':;' '\n' | while IFS= read -r file; do
target="$HOME/$file"
if [ -e "$target" ]; then
rm -rf "$target" && log "Removed: $file"
fi
done
log "Cleanup complete."
}
prompt_cleanup() {
printf "Do you want to clean up old configurations and history files? [y/N]: "
read -r response
case "$response" in
[yY][eE][sS] | [yY]) cleanup ;;
*) log "Skipping cleanup." ;;
esac
}
main() {
QUIET=0
CLEANUP=0
for arg in "$@"; do
case "$arg" in
-q | --quiet) QUIET=1 ;;
-c | --clean) CLEANUP=1 ;;
*) error "Invalid argument: $arg" ;;
esac
done
log "Setting up..."
log "* Verifying dependencies..."
check_dependencies
log "* Creating XDG directories..."
create_xdg_dirs
log "* Deploying Bash configuration..."
deploy_bash
log "* Installing Vim..."
deploy_vim
if [ "$CLEANUP" -eq 1 ]; then
cleanup
elif [ "$QUIET" -eq 0 ]; then
prompt_cleanup
fi
log "Setup complete! Restart your shell or run: exec bash"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment