graft (5443B)
1 #!/bin/bash 2 set -euo pipefail 3 4 USAGE="$( 5 cat <<EOF 6 Usage: $(basename "$0") [-n] ROOT PKGS... 7 EOF 8 )" 9 readonly USAGE 10 11 # ------------------------------------------------------------------------------ 12 # Utility 13 14 echo_lines() { 15 [ $# -ge 1 ] && printf "%s\n" "$@" 16 } 17 18 fail() { 19 echo_lines "$@" >&2 20 exit 1 21 } 22 23 # Resolve a path to an absolute path without dereferencing symlinks. 24 abspath() { realpath -ms "$@"; } 25 26 fg_color_code() { 27 case "$1" in 28 default) echo 39 ;; 29 30 black) echo 30 ;; 31 red) echo 31 ;; 32 green) echo 32 ;; 33 yellow) echo 33 ;; 34 blue) echo 34 ;; 35 magenta) echo 35 ;; 36 cyan) echo 36 ;; 37 white) echo 37 ;; 38 39 bright-black) echo 90 ;; 40 bright-red) echo 91 ;; 41 bright-green) echo 92 ;; 42 bright-yellow) echo 93 ;; 43 bright-blue) echo 94 ;; 44 bright-magenta) echo 95 ;; 45 bright-cyan) echo 96 ;; 46 bright-white) echo 97 ;; 47 esac 48 } 49 50 ansi_style() { 51 if [ -n "$USE_COLOR" ]; then 52 echo -en "\e[${1}m" 53 fi 54 } 55 ansi_reset() { ansi_style ""; } 56 57 start_color() { ansi_style "$(fg_color_code "$1")"; } 58 stop_color() { ansi_style 39; } 59 60 start_bold() { ansi_style 1; } 61 stop_bold() { ansi_style 22; } 62 63 # ------------------------------------------------------------------------------ 64 # Package API 65 66 msg() { 67 [ $# -ge 3 ] || fail "Usage: msg COLOR LABEL LINES..." 68 local color; color="$1" 69 local label; label="$2" 70 local first; first="$3" 71 shift 3 72 73 local prefix; prefix="$(stop_color)[$NAME] $(start_color "$color")" 74 echo "$prefix$(start_bold)$label$(stop_bold)$first" 75 echo_lines "${@/#/$prefix$(printf '%*s' ${#label} '')}" 76 stop_color 77 } 78 79 msg_note() { msg magenta "NOTE: " "$@"; } 80 msg_info() { msg default "INFO: " "$@"; } 81 msg_warning() { msg yellow "WARNING: " "$@"; } 82 msg_error() { msg red "ERROR: " "$@"; } 83 msg_fatal() { 84 msg bright-red "FATAL: " "$@" >&2 85 exit 1 86 } 87 msg_bad_package() { 88 [ $# -ge 1 ] || fail "Usage: msg_bad_package REASON CONTEXT..." 89 local reason; reason="$1" 90 shift 1 91 msg_fatal "bad package: $reason" "$@" 92 } 93 94 link() { 95 [ $# -eq 2 ] || [ $# -eq 1 ] || msg_bad_package "invalid link" "Usage: link DST_PATH SRC_PATH | link PATH" 96 local rel_dst; rel_dst="$1" 97 local rel_src; rel_src="${2:-$1}" 98 99 local abs_dst; abs_dst="$ROOT/$rel_dst" 100 local abs_src; abs_src="$PKG/$rel_src" 101 102 [ -e "$abs_src" ] || [ -h "$abs_src" ] || msg_bad_package "invalid link: source does not exist" 103 104 local move; move="" 105 if [ -e "$abs_dst" ] || [ -h "$abs_dst" ]; then 106 case "$(readlink -q "$abs_dst")" in 107 "$abs_src") 108 msg bright-black "LINK OK: " "$abs_dst --> $rel_src" 109 return 0 110 ;; 111 "$PKG"/*) 112 move=1 113 ;; 114 *) 115 msg red "LINK CONFLICT: " "$abs_dst -/-> $rel_src" 116 return 1 117 ;; 118 esac 119 fi 120 121 if [ -z "$DRY" ]; then 122 local err 123 124 local parent; parent="$(dirname "$abs_dst")" 125 if ! err="$(mkdir -p "$parent" 2>&1)"; then 126 msg red "LINK FAILED: " "$abs_dst -/-> $rel_src" "$err" 127 return 1 128 fi 129 130 local -a ln_opt=(); [ -n "$move" ] && ln_opt=("-f") 131 if ! err="$(ln -sn "${ln_opt[@]}" "$abs_src" "$abs_dst" 2>&1)"; then 132 msg red "LINK FAILED: " "$abs_dst -/-> $rel_src" "$err" 133 return 1 134 fi 135 fi 136 137 if [ -n "$move" ]; then 138 msg cyan "LINK MOVE: " "$abs_dst --> $rel_src" 139 else 140 msg blue "LINK CREATE: " "$abs_dst --> $rel_src" 141 fi 142 } 143 144 copy() { 145 [ $# -eq 2 ] || [ $# -eq 1 ] || msg_bad_package "invalid copy" "Usage: copy DST_PATH SRC_PATH | copy PATH" 146 local rel_dst; rel_dst="$1" 147 local rel_src; rel_src="${2:-$1}" 148 149 local abs_dst; abs_dst="$ROOT/$rel_dst" 150 local abs_src; abs_src="$PKG/$rel_src" 151 152 [ -e "$abs_src" ] || [ -h "$abs_src" ] || msg_bad_package "invalid copy: source does not exist" 153 154 if [ -e "$abs_dst" ] || [ -h "$abs_dst" ]; then 155 if diff -rq "$abs_dst" "$abs_src" >/dev/null 2>&1; then 156 msg bright-black "COPY OK: " "$abs_dst <-- $rel_src" 157 return 0 158 else 159 msg red "COPY CONFLICT: " "$abs_dst <-/- $rel_src" 160 return 1 161 fi 162 fi 163 164 if [ -z "$DRY" ]; then 165 local err 166 167 local parent; parent="$(dirname "$abs_dst")" 168 if ! err="$(mkdir -p "$parent" 2>&1)"; then 169 msg red "COPY FAILED: " "$abs_dst <-/- $rel_src" "$err" 170 return 1 171 fi 172 173 if ! err="$(cp -rpT "$abs_src" "$abs_dst" 2>&1)"; then 174 msg red "COPY FAILED: " "$abs_dst <-/- $rel_src" "$err" 175 return 1 176 fi 177 fi 178 179 msg green "COPY CREATE: " "$abs_dst <-- $rel_src" 180 } 181 182 # ------------------------------------------------------------------------------ 183 # Input Handling 184 185 # Options 186 opt_force_color="" 187 opt_dry="" 188 while getopts "cnh" opt; do 189 case "$opt" in 190 c) opt_force_color=1 ;; 191 n) opt_dry=1 ;; 192 h) echo "$USAGE"; exit 0 ;; 193 *) fail "$USAGE" ;; 194 esac 195 done 196 readonly opt_force_color 197 readonly opt_dry 198 shift $((OPTIND - 1)) 199 200 # Arguments 201 [ $# -ge 1 ] || fail "$USAGE" 202 arg_root="$1"; readonly arg_root 203 shift 1 204 arg_pkgs=("$@"); readonly arg_pkgs 205 206 # ------------------------------------------------------------------------------ 207 # Globals 208 209 DRY="$opt_dry"; readonly DRY 210 ROOT="$(abspath "$arg_root")"; readonly ROOT 211 212 USE_COLOR="" 213 if [ -z "${NO_COLOR-}" ] && [ -t 1 ] || [ -n "$opt_force_color" ]; then 214 USE_COLOR=1 215 fi 216 readonly USE_COLOR 217 218 # ------------------------------------------------------------------------------ 219 # Main 220 221 graft() { 222 local NAME; NAME="$1"; readonly NAME 223 local PKG; PKG="$(abspath "$NAME")"; readonly PKG 224 225 [ -d "$PKG" ] || msg_bad_package "not found: $PKG" 226 cd "$PKG" 227 [ -r .graft.sh ] || msg_bad_package "could not read .graft.sh" 228 set +e 229 source ./.graft.sh 230 return 0 231 } 232 233 [ -d "$ROOT" ] || fail "root is not a directory" 234 for p in "${arg_pkgs[@]}"; do 235 (graft "$p") || true # TODO: Count errors? 236 done