#!/usr/bin/env bash
#
#
#
#
# vim:ft=sh:et:ts=2:sw=2:sts=2:cc=80:
#
# sub.sh
# ~~~~~~
#
# Shortest way to provision a Linux environment as for my taste:
#
# $ curl -sL sub.sh | bash [-s - [~/.sub.sh] OPTIONS]
# $ wget -qO- sub.sh | bash [-s - [~/.sub.sh] OPTIONS]
#
# main(...)
main() {
parse_opts "$@"
init
check_os
# Go to the home directory. The current working directory may deny access
# from this user.
cd ~
setup_sudo
[[ "$lsb_dist" == ubuntu ]] && sudo -E apt-get update
setup_ssh
setup_basic
setup_os_specific
setup_python3
setup_tools
setup_zsh
download_subsh
setup_subsh
[[ -d "$bak_dir" ]] && info "Backup files are stored in: $bak_dir"
result
}
# help() prints the help message.
help() {
usage
>&2 echo
>&2 echo "Options:"
>&2 echo " --help Show this message and exit."
>&2 echo " --versions Show versions and exit."
>&2 echo " --no-pyenv Do not install pyenv."
}
# usage() prints the short usage message.
usage() {
>&2 echo "Usage: curl -sL sub.sh | bash [-s - [~/.sub.sh] OPTIONS]"
}
# parse_opts(...) parses options and sets variables.
# set: $subsh_dir, $install_pyenv
parse_opts() {
local dest_provided=false
subsh_dir=~/.sub.sh
install_pyenv=true
for opt in "$@"; do
case $opt in
--help)
help
exit
;;
--versions)
result
exit
;;
--no-pyenv)
install_pyenv=false
shift
;;
*)
local error=""
if [[ "$dest_provided" == true ]]; then
error="Too many arguments"
elif [[ "$opt" == -* ]]; then
error="Unknown option"
elif [[ -e "$opt" ]] && [[ ! -d "$opt" ]]; then
error="Non-directory exists"
fi
if [[ -n "$error" ]]; then
>&2 echo "$error: $opt"
usage
>&2 echo "Try --help for more information."
exit 2
fi
dest_provided=true
subsh_dir="$opt"
shift
;;
esac
done
readonly subsh_dir
readonly install_pyenv
}
# init() sets the static variables and environment variables.
# set: $lsb_dist, $timestamp, $bak_dir, $DEBIAN_FRONTEND
init() {
# shellcheck disable=SC1091
readonly lsb_dist="$(source /etc/os-release && echo "$ID")"
readonly timestamp="$(date +%s)"
readonly bak_dir="$subsh_dir/.bak.$timestamp"
# Disable interactive messages from apt-get.
if [[ "$lsb_dist" == ubuntu ]]; then
export DEBIAN_FRONTEND=noninteractive
fi
}
# check_os tests if the OS is either Ubuntu or CentOS.
check_os() {
if [[ "$lsb_dist" != ubuntu ]] && [[ "$lsb_dist" != centos ]]; then
error "Supporting Ubuntu or CentOS only. '$lsb_dist' is not supported."
exit 1
fi
}
# Utilities -------------------------------------------------------------------
# safe_tput(...) executes tput if running on a terminal.
if tput clear &>/dev/null; then
safe_tput() ( tput "$@" )
else
safe_tput() ( true )
fi
# info($text...) prints an information log with green color.
info() {
>&2 echo -en "$(safe_tput setaf 2)$(safe_tput rev) sub.sh $(safe_tput sgr0)"
>&2 echo -e "$(safe_tput setaf 2) $*$(safe_tput sgr0)"
}
# error($text...) prints an error log with red color.
error() {
>&2 echo -en "$(safe_tput setaf 1)$(safe_tput rev) sub.sh $(safe_tput sgr0)"
>&2 echo -e "$(safe_tput setaf 1) $*$(safe_tput sgr0)"
}
# executable($cmd) tests if the given command is executable.
executable() {
command -v "$1" &>/dev/null
}
# git_pull($src[, $dest]) pulls the remote Git repository. If the local
# repository does not exist, it clones instead.
git_pull() {
local src="$1"
local dest="${2:-"$(basename "$src")"}"
if [[ ! -d "$dest" ]]; then
mkdir -p "$dest"
git clone "$src" "$dest"
return
fi
git -C "$dest" pull --rebase --autostash
}
# link($src[, $dest]) creates a symbolic link. If the destination exists, it is
# moved into the backup directory.
link() {
local src="$1"
local dest="${2:-"$(basename "$src")"}"
if [[ -e $dest || -L $dest ]]; then
if [[ "$(readlink -f "$src")" == "$(readlink -f "$dest")" ]]; then
return
fi
# Backup the previous file.
mkdir -p "$bak_dir"
mv "$dest" "$bak_dir"
fi
mkdir -p "$(dirname "$dest")"
ln -vs "$src" "$dest"
}
# add_ppa($src) adds an APT repository at "ppa:$src". (Ubuntu-only)
add_ppa() { [[ "$lsb_dist" == ubuntu ]]
local src="$1"
if ! grep -q "^deb.*$src" /etc/apt/sources.list.d/*.list; then
sudo -E add-apt-repository -y "ppa:$src"
fi
}
# Provisioning ----------------------------------------------------------------
# setup_sudo() ensures that the sudo command is executable without password. If
# the password is required for the current user, exits with 1.
#
# NOTE: The system may rely on environment variables for sane functionality.
# For example, $http_proxy and $no_proxy for HTTP. Always use "sudo -E" to keep
# arbitrary environment variables.
#
setup_sudo() {
_install_sudo
_check_nopasswd_sudoer
}
_install_sudo() {
if executable sudo; then
info "sudo is already available."
return
fi
info "Installing sudo..."
case $lsb_dist in
ubuntu) apt-get update && apt-get install -y sudo ;;
centos) yum install -y sudo ;;
esac
}
_check_nopasswd_sudoer() {
if sudo &>/dev/null -n true; then
return
fi
local user
user="$(whoami)"
error "Make sure '$user' user may use sudo without password."
error
error " $ echo '$user aLL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-$user"
error
exit 1
}
# setup_ssh() ensures the localhost SSH connection is authorized without
# passphrase.
setup_ssh() {
_install_ssh_client
_install_ssh_server
_authorize_localhost_ssh
}
_install_ssh_client() {
if executable ssh; then
info "SSH client is already available."
return
fi
info "Installing SSH client..."
case $lsb_dist in
ubuntu) sudo -E apt-get install -y openssh-client ;;
centos) sudo -E yum install -y openssh-clients ;;
esac
}
_install_ssh_server() {
if (PATH="$PATH:/sbin" executable sshd); then
# └─ Some system does not append /sbin to $PATH.
info "SSH server is already available."
return
fi
info "Installing SSH server..."
case $lsb_dist in
ubuntu) sudo -E apt-get install -y openssh-server ;;
centos) sudo -E yum install -y openssh-server ;;
esac
}
_authorize_localhost_ssh() {
if ! systemctl status sshd &>/dev/null; then
info "SSH server is not running. Skipping to authorize localhost SSH."
return
fi
if ssh -qo BatchMode=yes localhost true; then
info "SSH to localhost is already authorized."
return
fi
mkdir -p ~/.ssh
if [[ ! -f ~/.ssh/id_rsa ]]; then
info "Generating ~/.ssh/id_rsa..."
ssh-keygen -f ~/.ssh/id_rsa -N ''
fi
if [[ ! -f ~/.ssh/id_rsa.pub ]]; then
info "Generating ~/.ssh/id_rsa.pub from ~/.ssh/id_rsa..."
ssh-keygen -y -f ~/.ssh/id_rsa >~/.ssh/id_rsa.pub
fi
info "Authorizing ~/.ssh/id_rsa.pub for localhost SSH connection..."
ssh-keyscan -H localhost 2>/dev/null 1>>~/.ssh/known_hosts
cat ~/.ssh/id_rsa.pub >>~/.ssh/authorized_keys
}
# setup_basic() installs basic utilities.
setup_basic() {
info "Installing basic utilities..."
case $lsb_dist in
ubuntu)
sudo -E apt-get install -y \
cmake curl htop iftop iputils-ping jq less lsof man net-tools ntpdate \
psmisc shellcheck software-properties-common telnet tree unzip wget
;;
centos)
sudo -E yum install -y epel-release
sudo -E yum install -y --setopt=skip_missing_names_on_install=False \
cmake curl htop iftop iputils jq less lsof man net-tools ntpdate \
psmisc ShellCheck telnet tree unzip wget
;;
esac
}
# setup_os_specific() installs packages only for the current OS.
setup_os_specific() {
case $lsb_dist in
ubuntu) _install_ubuntu_specific ;;
centos) _install_centos_specific ;;
esac
}
_install_ubuntu_specific() { [[ "$lsb_dist" == ubuntu ]]
info "Installing packages for Ubuntu..."
sudo -E apt-get install -y aptitude
# Generate en_US.UTF-8 locale to fix setlocale warnings.
sudo -E apt-get install -y locales
sudo locale-gen en_US.UTF-8
}
_install_centos_specific() { [[ "$lsb_dist" == centos ]]
info "Installing packages from CentOS..."
# End Point Package Repository
pushd "$(mktemp -d)"
wget https://packages.endpointdev.com/endpoint-rpmsign-7.pub
sudo -E rpm --import endpoint-rpmsign-7.pub
rpm -qi gpg-pubkey-703df089 | gpg --with-fingerprint
wget https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo-1.10-1.x86_64.rpm
sudo -E yum localinstall -y endpoint-repo-1.10-1.x86_64.rpm
# └─ "localinstall" exits with 0 even if already
# installed while "install" exits with 1.
popd
# COPR
sudo -E yum install -y yum-plugin-copr
}
# setup_python3() installs Python 3.6+.
setup_python3() {
info "Installing Python 3..."
case $lsb_dist in
ubuntu) sudo -E apt-get install -y python3 python3-dev python3-setuptools python-dev-is-python3 ;;
centos) sudo -E yum install -y python3 python3-devel python3-setuptools ;;
esac
}
# setup_tools() installs tmux 2.6+, Git 2+, Vim 8+, ripgrep, and fd.
setup_tools() {
_install_tmux
_install_git
_install_vim
_install_rg
_install_fd
[[ "$install_pyenv" = true ]] && _install_pyenv
}
_install_tmux() {
info "Installing tmux..."
case $lsb_dist in
ubuntu) sudo -E apt-get install -y tmux ;;
centos) sudo -E yum install -y tmux ;; # tmux 2.9a from End Point
esac
}
_install_git() {
info "Installing Git..."
case $lsb_dist in
ubuntu)
add_ppa git-core/ppa
sudo -E apt-get update
sudo -E apt-get install -y git
;;
centos)
sudo -E yum install -y git # git 2.24.1 from End Point
;;
esac
}
_install_vim() {
info "Installing Vim..."
case $lsb_dist in
ubuntu)
add_ppa jonathonf/vim
sudo -E apt-get update
sudo -E apt-get install -y vim
;;
centos)
sudo -E yum copr -y enable hnakamur/vim
sudo -E yum update -y vim-common vim-minimal
sudo -E yum install -y vim
;;
esac
}
_install_rg() {
info "Installing ripgrep..."
case $lsb_dist in
ubuntu)
sudo -E apt-get install ripgrep
;;
centos)
sudo -E yum copr -y enable carlwgeorge/ripgrep
sudo -E yum install -y ripgrep
;;
esac
}
_install_fd() {
info "Installing fd..."
case $lsb_dist in
ubuntu)
sudo -E apt-get install fd-find
sudo ln -fs "$(command -v fdfind)" /usr/local/bin/fd
;;
centos)
pushd "$(mktemp -d)"
curl -LO "https://github.com/sharkdp/fd/releases/download/v8.2.1/fd-v8.2.1-$(arch)-unknown-linux-musl.tar.gz"
tar xzf ./*.tar.gz
sudo cp fd-*/fd /usr/local/bin/fd
popd
;;
esac
}
_install_pyenv() { [[ "$install_pyenv" = true ]]
if executable pyenv; then
info "pyenv is already available."
return
fi
info "Installing pyenv..."
curl -L https://git.io/vxZax | bash
}
# setup_zsh() installs ZSH, Oh My ZSH!, and third-party plugins. It depends on
# Git.
setup_zsh() {
_install_zsh
_install_ohmyzsh
sudo -E chsh -s "$(command -v zsh)" "$(whoami)"
}
_install_zsh() {
info "Installing ZSH..."
case $lsb_dist in
ubuntu) sudo -E apt-get install -y zsh ;;
centos) sudo -E yum install -y zsh ;;
esac
}
_install_ohmyzsh() {
info "Installing Oh My ZSH!..."
git_pull https://github.com/robbyrussell/oh-my-zsh ~/.oh-my-zsh
pushd ~/.oh-my-zsh/custom/plugins
git_pull https://github.com/zsh-users/zsh-syntax-highlighting
git_pull https://github.com/zsh-users/zsh-autosuggestions
git_pull https://github.com/bobthecow/git-flow-completion
popd
}
# sub.sh ----------------------------------------------------------------------
# download_subsh() clones the sub.sh repository at the given target directory.
download_subsh() {
info "Downloading sub.sh at $subsh_dir..."
git_pull https://github.com/sublee/sub.sh "$subsh_dir"
}
# setup_subsh() enables the settings from sub.sh.
setup_subsh() {
_apply_settings
_install_vim_plugins
_install_tmux_plugins
}
_apply_settings() {
info "Applying settings from $subsh_dir..."
git config --global include.path "$subsh_dir/git-aliases"
link "$subsh_dir/profile" ~/.profile
link "$subsh_dir/zshrc" ~/.zshrc
link "$subsh_dir/subsh.zsh-theme" ~/.oh-my-zsh/custom/subsh.zsh-theme
link "$subsh_dir/vimrc" ~/.vimrc
link "$subsh_dir/tmux.conf" ~/.tmux.conf && (tmux source ~/.tmux.conf || true)
link "$subsh_dir/python-startup.py" ~/.python-startup
mkdir -p ~/.ipython/profile_default
pushd ~/.ipython/profile_default
link "$subsh_dir/ipython_config.py"
popd
}
_install_vim_plugins() {
info "Installing Vim plugins..."
# Vim-Plug
if [[ ! -f ~/.vim/autoload/plug.vim ]]; then
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
fi
vim --noplugin -c PlugInstall -c qa &>/dev/null
}
_install_tmux_plugins() {
info "Installing tmux plugins..."
mkdir -p ~/.tmux/plugins
pushd ~/.tmux/plugins
git_pull https://github.com/tmux-plugins/tpm
TMUX_PLUGIN_MANAGER_PATH=~/.tmux/plugins/ ./tpm/scripts/install_plugins.sh
popd
}
# result() prints the information of provisioning result.
result() {
echo
_print_emblem
_print_versions
echo
}
_print_emblem() {
if [[ -n "$TERM" ]]; then
cat "$subsh_dir/emblem.txt"
fi
}
_print_versions() {
local subsh_version git_version vim_version rg_version fd_version
PATH="$PATH:/usr/local/bin"
subsh_version="$(git -C "$subsh_dir" rev-parse --short HEAD)"
zsh_version="$(zsh --version | awk '{ print $2 }')"
tmux_version="$(tmux -V | awk '{ print $2 }')"
git_version="$(git --version | awk '{ print $3 }')"
vim_version="$(vim --version | awk '{ print $5; exit }')"
rg_version="$(rg --version | tail -n +1 | head -n 1 | cut -d' ' -f2)"
fd_version="$(fd --version | cut -d' ' -f2)"
python_version="$(python3 --version | awk '{ print $2 }')"
echo "sub.sh: $subsh_version at $subsh_dir"
echo -n "zsh-$zsh_version "
echo -n "tmux-$tmux_version "
echo -n "git-$git_version "
echo -n "vim-$vim_version "
echo -n "rg-$rg_version "
echo -n "fd-$fd_version "
echo -n "python-$python_version"
echo
}
# Entrypoint ------------------------------------------------------------------
set -euo pipefail
trap 'error "Interrupted during provisioning."; exit 1' INT
trap 'error "Failed to provision."; exit 1' ERR
main "$@"