diff --git a/fish/install.sh b/fish/install.sh index 7597128..cb8b9ed 100755 --- a/fish/install.sh +++ b/fish/install.sh @@ -1,9 +1,11 @@ -#!/usr/bin/env bash -# vim:set ft=bash: +#!/usr/bin/env sh -cd "$(dirname "${BASH_SOURCE[0]}")" \ - && source "../homebrew/brew_utils.sh" \ - && source "../script/utils.sh" +DOT="${DOT:-$HOME/dotfiles}" + +# shellcheck source=../homebrew/brew_utils.sh +. "$DOT/homebrew/brew_utils.sh" +# shellcheck source=../script/utils.sh +. "$DOT/script/utils.sh" # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11,15 +13,11 @@ brew_install "Fish Shell" "fish" fish_path="$(which fish)" -if ! grep "$fish_path" < /etc/shells &> /dev/null; then +if ! grep "$fish_path" < /etc/shells >/dev/null 2>&1; then execute \ "printf '%s\n' '$fish_path' | sudo tee -a /etc/shells" \ "Add '$fish_path' to '/etc/shells'" fi -chsh -s "$fish_path" &> /dev/null +chsh -s "$fish_path" >/dev/null 2>&1 print_result $? "Make OS use Fish as the default shell" - -execute \ - "curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher" \ - "Fisher" diff --git a/script/bootstrap.sh b/script/bootstrap.sh new file mode 100755 index 0000000..75d6b10 --- /dev/null +++ b/script/bootstrap.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env sh + +readonly GITHUB_REPO="punkfairie/dotfiles" + +readonly DOTFILES_ORIGIN="git@github.com:$GITHUB_REPO.git" +readonly DOTFILES_TARBALL="https://github.com/$GITHUB_REPO/tarball/main" +readonly DOTFILES_UTILS="https://raw.githubusercontent.com/$GITHUB_REPO/main/scripts/utils.sh" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +export DOT="$HOME/dotfiles" # MUST HAVE NO TRAILING SLASH!! +yes_to_all=false + +################################################################################ +# Download Dotfiles # +################################################################################ + +download() +{ + url="$1" + output="$2" + + if command -v "curl" >/dev/null 2>&1; then + curl \ + --location \ + --silent \ + --show-error \ + --output "$output" \ + "$url" \ + >/dev/null 2>&1 + + return $? + + elif command -v "wget" >/dev/null 2>&1; then + wget \ + --quiet \ + --output-document="$output" \ + "$url" \ + >/dev/null 2>&1 + + return $? + fi + + return 1 +} + +download_utils() +{ + tmp_file="$(mktemp /tmp/XXXXX)" + + # shellcheck source=/dev/null + download "$DOTFILES_UTILS" "$tmp_file" \ + && . "$tmp_file" \ + && rm -rf "$tmp_file" \ + && return 0 + + return 1 +} + +extract() +{ + archive="$1" + output_dir="$2" + + if command -v "tar" >/dev/null 2>&1; then + tar \ + --extract \ + --gzip \ + --file "$archive" \ + --strip-components 1 \ + --directory "$output_dir" + + return $? + fi + + return 1 +} + +download_dotfiles() +{ + print_title "Download and extract dotfiles archive" + + tmp_file="$(mktemp /tmp/XXXXX)" + + download "$DOTFILES_TARBALL" "$tmp_file" + print_result $? "Download archive" "true" + printf "\n" + + if ! $yes_to_all; then + + while [ -e "$DOT" ]; do + ask "'$DOT' already exists, do you want to (o)verwrite or (b)ackup the existing directory?" + answer="$(get_answer)" + + case $answer in + o ) rm -rf "$DOT"; break;; + b ) mv "$DOT" "$DOT.bak"; break;; + * ) print_warning "Please enter a valid option." + esac + + done + + else + rm -rf "$DOT" >/dev/null 2>&1 + fi + + mkdir -p "$DOT" + print_result $? "Create '$DOT'" "true" + + # Extract archive. + extract "$tmp_file" "$DOT" + print_result $? "Extract archive" "true" + + rm -rf "$tmp_file" + print_result $? "Remove archive" + + cd "$DOT/script" \ + || return 1 +} + +################################################################################ +# Xcode # +################################################################################ + +are_xcode_cli_tools_installed() +{ + xcode-select --print-path >/dev/null 2>&1 +} + +install_xcode_cli_toools() +{ + if [ "$(uname)" = "Darwin" ]; then + + print_title "Xcode" + + xcode-select --install >/dev/null 2>&1 + + execute \ + "until are_xcode_cli_tools_installed; do sleep 5; done" \ + "Install Xcode Command Line Tools" + + sudo xcodebuild -license accept >/dev/null 2>&1 + print_result $? "Agree to the terms of the Xcode license" + fi +} + +################################################################################ +# Setup Gitconfig # +################################################################################ + +setup_gitconfig() +{ + cd "$DOT" || return + + if ! [ -f "$DOT/git/.gitconfig.local.symlink" ]; then + print_title "Set up gitconfig" + + git_credential="cache" + + if [ "$(uname)" = "Darwin" ]; then + git_credential="osxkeychain" + fi + + print_question "What is your Github author name?" + read -r git_authorname + + print_question "What is your Github author email?" + read -r git_authoremail + + sed -e "s/AUTHORNAME/$git_authorname/g" \ + -e "s/AUTHOREMAIL/$git_authoremail/g" \ + -e "s/GIT_CREDENTIAL_HELPER/$git_credential/g" \ + "$DOT/git/.gitconfig.local.symlink.example" > "$DOT/.gitconfig.local.symlink" + + print_result $? "gitconfig" + fi +} + +################################################################################ +# Initialize Git Repo # +################################################################################ + +git_init() +{ + print_title "Initialize Git repository" + + if [ -z "$DOTFILES_ORIGIN" ]; then + print_error "Please provide a URL for the Git origin" + return 1 + fi + + if ! is_git_repository; then + cd "$DOT" || print_error "Failed to cd $DOT" + + execute \ + "git init && git remote add origin $DOTFILES_ORIGIN" \ + "Initialize the dotfiles Git repository" + fi +} + +################################################################################ +# Main # +################################################################################ + +main() +{ + if [ "$(uname)" != "Linux" ] && [ "$(uname)" != "Darwin" ]; then + printf "Sorry, this script is intended only for macOS and Ubuntu!" + return 1 + fi + + # Load utils. + + if [ -x "${DOT}/script/utils.sh" ]; then + . "${DOT}/script/utils.sh" || exit 1 + else + download_utils || exit 1 + fi + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + skip_questions "$@" \ + && yes_to_all=true + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + ask_for_sudo + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + # Check if this script was run directly, and if not, dotfiles will need to be + # downloaded. + + printf "%s" "$(sh_source "$0")" | grep "bootstrap.sh" >/dev/null 2>&1 \ + || download_dotfiles + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + setup_gitconfig + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + install_xcode_cli_toools + + "$DOT/homebrew/brew.sh" + + "$DOT/fish/install.sh" + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + if cmd_exists "git"; then + if [ "$(git config --get remote.origin.url)" != "$DOTFILES_ORIGIN" ]; then + git_init + fi + fi +} + +main "$@" diff --git a/script/dot b/script/dot new file mode 100644 index 0000000..c02e6cd --- /dev/null +++ b/script/dot @@ -0,0 +1,119 @@ +#!/usr/bin/env fish +# vim:set ft=fish : + +set -q DOT || set -gx DOT "$HOME/dotfiles" + +source "$DOT/script/utils" + +################################################################################ +# OS Preferences # +################################################################################ + +function set_os_prefs + print_title "OS Preferences" + + set os $(uname | string lower) + + "$DOT/os/$os.fish" +end + +################################################################################ +# Symlink Dotfiles # +################################################################################ + +function link_file + set -f src $argv[1] + set -f dst $argv[2] + + set -f overwrite false + set -f backup false + set -f skip false + set -f action + + if [ -f "$dst" ] || [ -d "$dst" ] || [ -L "$dst" ] + + if ! $overwrite_all && ! $backup_all && ! $skip_all + set -f current_src (readlink "$dst") + + if [ "$current_src" = "$src" ] + set skip true + else + print_question "File already exists: $dst ($(basename $src)), what do you want to do?\n([s]kip, [S]kip all, [o]verwrite, [O]verwrite all, [b]ackup, [B]ackup all)" + read -n 1 action + + switch $action + case o + set overwrite true + case O + set overwrite_all true + case b + set backup true + case B + set backup_all true + case s + set skip true + case S + set skip_all true + end + end + end + + set -q overwrite || set overwrite $overwrite_all + set -q backup || set backup $backup_all + set -q skip || set skip $skip_all + + if $overwrite + rm -rf "$dst" + print_success "Removed $dst" + end + + if $backup + mv "$dst" "{$dst}.bak" + print_success "Moved $dst to {$dst}.bak" + end + + if $skip + print_success "Skipped $src" + end + end + + if ! $skip + # See of any directories need to be created. + if echo "$dst" | grep -q '/' 2> /dev/null + mkdir -p (string replace -r '\/[^\/]+$' '' "$dst") + end + + ln -s "$src" "$dst" + print_success "Linked $src to $dst" + end +end + +function make_dst + set -l path (string escape --style=regex "$DOT") + set -l regex (string join '^\/' $path '\/[a-zA-Z]+\/(.+)\.symlink$') + set -l dst (string replace -r $regex '$1' "$argv[1]") + + printf '%s' "$dst" +end + +function install_dotfiles + print_title "Installing Dotfiles" + + set -g overwrite_all false + set -g backup_all false + set -g skip_all false + + find -H "$DOT" -name "*.symlink" -not -path ".git" | \ + while read -l -t src; link_file "$src" "$(make_dst $src)"; end +end + +################################################################################ +# Main # +################################################################################ + +print_title "Installers" +find . -name install.fish | while read installer; fish -c "$installer"; end + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +install_dotfiles diff --git a/script/dot.sh b/script/dot.sh deleted file mode 100755 index 11b0537..0000000 --- a/script/dot.sh +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env bash -# vim:set ft=bash: - -declare -r GITHUB_REPO="punkfairie/dotfiles" - -declare -r DOTFILES_ORIGIN="git@github.com:$GITHUB_REPO.git" -declare -r DOTFILES_TARBALL="https://github.com/$GITHUB_REPO/tarball/main" -declare -r DOTFILES_UTILS="https://raw.githubusercontent.com/$GITHUB_REPO/main/scripts/utils.sh" - -# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -declare dotfiles_dir="$HOME/dotfiles" # MUST HAVE NO LEADING SLASH!! -declare yes_to_all=false - -################################################################################ -# Download Dotfiles # -################################################################################ - -download() -{ - local url="$1" - local output="$2" - - if command -v "curl" &> /dev/null; then - curl \ - --location \ - --silent \ - --show-error \ - --output "$output" \ - "$url" \ - &> /dev/null - - return $? - - elif command -v "wget" &> /dev/null; then - wget \ - --quiet \ - --output-document="$output" \ - "$url" \ - &> /dev/null - - return $? - fi - - return 1 -} - -download_utils() -{ - local tmp_file="" - - tmp_file="$(mktemp /tmp/XXXXX)" - - download "$DOTFILES_UTILS" "$tmp_file" \ - && . "$tmp_file" \ - && rm -rf "$tmp_file" \ - && return 0 - - return 1 -} - -extract() -{ - local archive="$1" - local output_dir="$2" - - if command -v "tar" &> /dev/null; then - tar \ - --extract \ - --gzip \ - --file "$archive" \ - --strip-components 1 \ - --directory "$output_dir" - - return $? - fi - - return 1 -} - -download_dotfiles() -{ - local tmp_file="" - - print_title "Download and extract dotfiles archive" - - tmp_file="$(mktemp /tmp/XXXXX)" - - download "$DOTFILES_TARBALL" "$tmp_file" - print_result $? "Download archive" "true" - printf "\n" - - if ! $yes_to_all; then - - while [[ -e $dotfiles_dir ]]; do - ask "'$dotfiles_dir' already exists, do you want to (o)verwrite or (b)ackup the existing directory?" - answer="$(get_answer)" - - case $answer in - o ) rm -rf "$dotfiles_dir"; break;; - b ) mv "$dotfiles_dir" "$dotfiles_dir.bak"; break;; - * ) print_warning "Please enter a valid option." - esac - - done - - else - rm -rf "$dotfiles_dir" &> /dev/null - fi - - mkdir -p "$dotfiles_dir" - print_result $? "Create '$dotfiles_dir'" "true" - - # Extract archive. - extract "$tmp_file" "$dotfiles_dir" - print_result $? "Extract archive" "true" - - rm -rf "$tmp_file" - print_result $? "Remove archive" - - cd "$dotfiles_dir/script" \ - || return 1 -} - -################################################################################ -# Xcode # -################################################################################ - -are_xcode_cli_tools_installed() -{ - xcode-select --print-path &> /dev/null -} - -install_xcode_cli_toools() -{ - if [[ "$(uname)" == "Darwin" ]]; then - - print_title "Xcode" - - xcode-select --install &> /dev/null - - execute \ - "until are_xcode_cli_tools_installed; do sleep 5; done" \ - "Install Xcode Command Line Tools" - - sudo xcodebuild -license accept &> /dev/null - print_result $? "Agree to the terms of the Xcode license" - fi -} - -################################################################################ -# Setup Gitconfig # -################################################################################ - -setup_gitconfig() -{ - cd "$dotfiles_dir" - - if ! [[ -f $dotfiles_dir/git/.gitconfig.local.symlink ]]; then - print_title "Set up gitconfig" - - git_credential="cache" - - if [[ "$(uname)" == "Darwin" ]]; then - git_credential="osxkeychain" - fi - - print_question "What is your Github author name?" - read -e git_authorname - - print_question "What is your Github author email?" - read -e git_authoremail - - sed -e "s/AUTHORNAME/$git_authorname/g" \ - -e "s/AUTHOREMAIL/$git_authoremail/g" \ - -e "s/GIT_CREDENTIAL_HELPER/$git_credential/g" \ - $dotfiles_dir/git/.gitconfig.local.symlink.example > $dotfiles_dir/.gitconfig.local.symlink - - print_result $? "gitconfig" - fi -} - -################################################################################ -# Install Dotfiles # -################################################################################ - -link_file() -{ - local src=$1 - local dst=$2 - - local overwrite= - local backup= - local skip= - local action= - - if [ -f "$dst" -o -d "$dst" -o -L "$dst" ]; then - - if ! $overwrite_all && ! $backup_all && ! $skip_all; then - local current_src="$(readlink $dst)" - - if [[ "$current_src" == "$src" ]]; then - skip=true - - else - print_question "File already exists: $dst ($(basename "$src")), what do you want to do?\n\ - [s]kip, [S]kip all, [o]verwrite, [O]verwrite all, [b]ackup, [B]ackup all?" - read -n 1 action - - case "$action" in - o ) overwrite=true ;; - O ) overwrite_all=true ;; - b ) backup=true ;; - B ) backup_all=true ;; - s ) skip=true ;; - S ) skip_all=true ;; - * ) ;; - esac - fi - - fi - - overwrite=${overwrite:-$overwrite_all} - backup=${backup:-$backup_all} - skip=${skip:-$skip_all} - - if $overwrite; then - rm -rf "$dst" - print_success "Removed $dst" - fi - - if $backup; then - mv "$dst" "${dst}.bak" - print_success "Moved $dst to ${dst}.bak" - fi - - if $skip; then - print_success "Skipped $src" - fi - - fi - - if [[ $skip != "true" ]]; then # "false" or empty - - # See if any directories need to be created - if [[ "$dst" =~ '/' ]]; then - mkdir -p "${dst%/*}" - fi - - ln -s "$src" "$dst" - print_success "Linked $src to $dst" - fi -} - -install_dotfiles() -{ - print_title "Installing dotfiles" - - local overwrite_all=false - local backup_all=false - local skip_all=false - - for src in $(find -H "$dotfiles_dir" -maxdepth 5 -name "*.symlink" -not -path ".git"); do - dst="${src%.*}" - dst="${dst#$dotfiles_dir/}" - dst="${dst#*/}" - dst="$HOME/$dst" - link_file "$src" "$dst" - done -} - -################################################################################ -# Initialize Git Repo # -################################################################################ - -git_init() -{ - print_title "Initialize Git repository" - - if [[ -z "$DOTFILES_ORIGIN" ]]; then - print_error "Please provide a URL for the Git origin" - return 1 - fi - - if ! is_git_repository; then - cd $dotfiles_dir || print_error "Failed to cd $dotfiles_dir" - - execute \ - "git init && git remote add origin $DOTFILES_ORIGIN" \ - "Initialize the dotfiles Git repository" - fi -} - -################################################################################ -# Restart OS # -################################################################################ - -restart_os() -{ - print_title "Restart" - - ask_for_confirmation "Do you want to restart?" - printf "\n" - - if answer_is_yes; then - sudo shutdown -r now &> /dev/null - fi -} - -################################################################################ -# Main # -################################################################################ - -main() -{ - if [[ "$(uname)" != "Linux" ]] && [[ "$(uname)" != "Darwin" ]]; then - printf "Sorry, this script is intended only for macOS and Ubuntu!" - return 1 - fi - - # Load utils. - - if [[ -x "${dotfiles_dir}/script/utils.sh" ]]; then - . "${dotfiles_dir}/script/utils.sh" || exit 1 - else - download_utils || exit 1 - fi - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - skip_questions "$@" \ - && yes_to_all=true - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ask_for_sudo - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Check if this script was run directly, and if not, dotfiles will need to be - # downloaded. - - printf "%s" "${BASH_SOURCE[0]}" | grep "dot.sh" &> /dev/null \ - || download_dotfiles - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setup_gitconfig - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - install_dotfiles - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $dotfiles_dir/os/pref.sh - - install_xcode_cli_toools - - $dotfiles_dir/homebrew/brew.sh - - $dotfiles_dir/node/volta.sh - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - print_title "Installers" - find . -name install.sh | while read installer ; do sh -c "${installer}" ; done - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if cmd_exists "git"; then - if [[ "$(git config --get remote.origin.url)" != "$DOTFILES_ORIGIN" ]]; then - git_init - fi - fi - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if ! $yes_to_all; then - restart_os - fi -} - -main "$@" diff --git a/script/utils b/script/utils new file mode 100644 index 0000000..710e206 --- /dev/null +++ b/script/utils @@ -0,0 +1,141 @@ +#!/usr/bin/env fish + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function print_in_color + printf '%b' \ + "$(tput setaf $argv[2] 2> /dev/null)" \ + $argv[1] \ + "$(tput sgr0 2> /dev/null)" +end + +function print_in_red + print_in_color $argv[1] 1 +end + +function print_in_yellow + print_in_color $argv[1] 3 +end + +function print_in_green + print_in_color $argv[1] 2 +end + +function print_in_purple + print_in_color $argv[1] 5 +end + +function print_title + print_in_purple "\n • $argv[1]\n\n" +end + +function print_success + print_in_green " [✔] $argv[1]\n" +end + +function print_warning + print_in_yellow " [!] $argv[1]\n" +end + +function print_error + print_in_red " [✖] $argv[1] $argv[2]\n" +end + +function print_question + print_in_yellow " [?] $argv[1]\n" +end + +function print_result + if [ "$argv[1]" -eq 0 ] + print_success $argv[2] + else + print_error $argv[2] + end + + return $argv[1] +end + +function print_error_stream + while read -r line + print_error "↳ ERROR: $line" + end +end + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function show_spinner + set -l frames '/-\|' + + set -l number_of_frames (string length $FRAMES) + + set -l pid $argv[1] + set -l cmds $argv[2] + set -l msg $argv[3] + + set -l i 0 + set -l frame_text "" + + printf "\n\n\n" + tput cuu 3 + tput sc + + while kill -0 "$pid" &> /dev/null + set i math $i + 1 + set -l num math % $number_of_frames + set -l frame (string sub -s $num -l 1) + set frame_text " [$frame] $msg" + + printf '%s' $frame_text + + sleep 0.2 + + tput rc + end +end + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function set_trap + trap -p "$argv[1]" | grep "$argv[2]" &> /dev/null \ + || trap "$argv[2]" "$argv[1]" +end + +function kill_all_subproccesses + set -l i "" + + for i in (jobs -p) + kill "$i" + wait "$i" &> /dev/null + end +end + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +function execute + set -l cmds $argv[1] + set -q $argv[2] and set -l msg $argv[2] or set -l msg $argv[1] + + set -l tmp_file "$(mktmp /tmp/XXXXX)" + + set -l exit_code 0 + + set_trap 'EXIT' 'kill_all_subproccesses' + + eval "$cmds" &> /dev/null 2> $tmp_file & + set cmds_pid $last_pid + + show_spinner $cmds_pid $cmds $msg + + wait $cmds_pid &> /dev/null + set exit_code $status + + print_result $exit_code $msg + + if [ $exit_code -ne 0 ] + print_error_stream < $tmp_file + end + + rm -rf "$tmp_file" + + return $exit_code +end diff --git a/script/utils.sh b/script/utils.sh index 62319e7..0a4806c 100755 --- a/script/utils.sh +++ b/script/utils.sh @@ -1,5 +1,4 @@ -#!/usr/bin/env bash -# vim:set ft=bash: +#!/usr/bin/env sh # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -21,7 +20,7 @@ skip_questions() ask_for_sudo() { - sudo -v &> /dev/null + sudo -v >/dev/null 2>&1 # Update existing 'sudo' timestamp until this script has finished. # @@ -31,7 +30,7 @@ ask_for_sudo() sudo -n true sleep 60 kill -0 "$$" || exit - done &> /dev/null & + done >/dev/null 2>&1 & } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -93,7 +92,7 @@ print_question() print_result() { - if [[ "$1" == 0 ]]; then + if [ "$1" = 0 ]; then print_success "$2" else print_error "$2" @@ -110,6 +109,17 @@ print_error_stream() } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# https://stackoverflow.com/a/32270158 +# POSIX read -n 1 +# Usage: var="$(read_char)" +read_char() +{ + old=$(stty -g) + stty raw -echo min 0 + printf '%s' "$(dd bs=1 count=1 2>/dev/null)" + stty "$old" +} + ask() { print_question "$1" @@ -124,32 +134,29 @@ get_answer() ask_for_confirmation() { print_question "$1 (y/n) " - read -r -n 1 + REPLY="$(read_char)" printf "\n" } answer_is_yes() { - [[ "$REPLY" =~ ^[Yy]$ ]] \ - && return 0 \ - || return 1 + expr "$REPLY" : '[Yy]$' } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - show_spinner() { - local -r FRAMES='/-\|' + readonly FRAMES='/-\|' - # shellcheck disable=SC2034 - local -r NUMBER_OF_FRAMES=${#FRAMES} + readonly NUMBER_OF_FRAMES=${#FRAMES} - local -r CMDS="$2" - local -r MSG="$3" - local -r PID="$1" + readonly CMDS="$2" + readonly MSG="$3" + readonly PID="$1" - local i=0 - local frame_text="" + i=0 + frame_text="" # Provide more space so that the text hopefully doesn't reach the bottom line # of the terminal window. @@ -163,8 +170,11 @@ show_spinner() tput cuu 3 tput sc - while kill -0 "$PID" &> /dev/null; do - frame_text=" [${FRAMES:i++%NUMBER_OF_FRAMES:1}] $MSG" + while kill -0 "$PID" >/dev/null 2>&1; do + i=$((i+1)) + num=$((i % NUMBER_OF_FRAMES)) + frame="$(echo $FRAMES | cut -c ${num}-$((num + 1)))" + frame_text=" [$frame] $MSG" # Print frame text. printf "%s" "$frame_text" @@ -180,40 +190,87 @@ show_spinner() cmd_exists() { - command -v "$1" &> /dev/null + command -v "$1" >/dev/null 2>&1 } is_git_repository() { - git rev-parse &> /dev/null + git rev-parse >/dev/null 2>&1 +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# https://stackoverflow.com/a/29835459 +# POSIX compliant way of finding BASH_SOURCE +# Usage: sh_source "$0" +rreadlink() +( + target=$1 + fname= + targetDir= + CDPATH= + + { \unalias command; \unset-f command; } >/dev/null 2>&1 + # shellcheck disable=SC2034 + [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on + + while :; do + [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: $target does not exist." >&2; return 1; } + # shellcheck disable=SC2164 + command cd "$(command dirname -- "$target")" + fname=$(command basename -- "$target") + [ "$fname" = '/' ] && fname='' + + if [ -L "$fname" ]; then + target=$(command ls -l "$fname") + target=${target#* -> } + continue + fi + + break + done + + targetDir=$(command pwd -P) + if [ "$fname" = '.' ]; then + command printf '%s\n' "${targetDir%/}" + elif [ "$fname" = '..' ]; then + command printf '%s\n' "${targetDir%/}/$fname" + fi +) + +sh_source() +{ + printf '%s' "$(dirname -- "$(rreadlink "$1")")" } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - set_trap() { - trap -p "$1" | grep "$2" &> /dev/null \ + trap | grep "'$2' $1" >/dev/null 2>&1 \ || trap '$2' "$1" } kill_all_subproccesses() { - local i="" + i="" for i in $(jobs -p); do kill "$i" - wait "$i" &> /dev/null + wait "$i" >/dev/null 2>&1 done } execute() { - local -r CMDS="$1" - local -r MSG="${2:-$1}" - local -r TMP_FILE="$(mktemp /tmp/XXXXX)" + readonly CMDS="$1" + readonly MSG="${2:-$1}" - local exit_code=0 - local cmds_pid="" + readonly TMP_FILE + TMP_FILE="$(mktemp /tmp/XXXXX)" + + exit_code=0 + cmds_pid="" # If the current process is ended, also end all its subproccesses. set_trap "EXIT" "kill_all_subproccesses" @@ -221,7 +278,7 @@ execute() # Execute commands in background # shellcheck disable=SC2261 eval "$CMDS" \ - &> /dev/null \ + >/dev/null 2>&1 \ 2> "$TMP_FILE" & cmds_pid=$! @@ -231,13 +288,13 @@ execute() # Wait for the commands to no longer be executing in the background, and then # get their exit code. - wait "$cmds_pid" &> /dev/null + wait "$cmds_pid" >/dev/null 2>&1 exit_code=$? # Print output based on what happened. print_result $exit_code "$MSG" - if [[ $exit_code -ne 0 ]]; then + if [ $exit_code -ne 0 ]; then print_error_stream < "$TMP_FILE" fi