├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── README.md ├── bin ├── favicon.sh ├── alias.sh ├── thumb.sh ├── add-movie-subtitles.sh ├── nfs-exports.sh ├── reload-browser.sh ├── buildkite-display-inline.sh ├── aws-ec2-private-ip.sh ├── replace.sh ├── docker-image-exists.sh ├── cert-expiration-dates.sh ├── apply-commit-times.sh ├── forward-ports.sh ├── log.sh ├── wait-for-files.sh ├── wait-for-hostnames.sh ├── wait-for-hosts.sh ├── superd.sh ├── android-emulator.sh ├── aws-website-redirect.sh ├── github-release.sh ├── hostnames.sh ├── parallelize.sh ├── docker-build-images.sh ├── envconfig.sh ├── mail.sh └── pull-repository.sh └── LICENSE.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [blueimp] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: shellcheck bin/*.sh 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shell Scripts 2 | A collection of (mostly POSIX compatible) shell scripts. 3 | 4 | ## License 5 | Released under the [MIT license](https://opensource.org/licenses/MIT). 6 | 7 | ## Author 8 | [Sebastian Tschan](https://blueimp.net/) 9 | -------------------------------------------------------------------------------- /bin/favicon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Creates a favicon for a given image. 5 | # 6 | # Usage: ./favicon.sh image_source [image_destination] 7 | # 8 | # Requires the ImageMagick convert binary to be installed. 9 | # 10 | 11 | convert \ 12 | -density 384 \ 13 | -colors 256 \ 14 | -background transparent \ 15 | -define icon:auto-resize=32,16 \ 16 | "${1?}" \ 17 | "${2:-favicon.ico}" 18 | -------------------------------------------------------------------------------- /bin/alias.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Useful alias commands. 5 | # 6 | # Usage: . ./alias.sh 7 | # 8 | 9 | # Simple static files web server: 10 | alias srv='python3 -m http.server --bind 127.0.0.1' 11 | 12 | # Print listening services: 13 | alias listening='lsof -iTCP -sTCP:LISTEN -n -P +c0' 14 | 15 | # Prints a random alphanumeric characters string with the given length: 16 | alias random='LC_ALL=C tr -dc A-Za-z0-9 < /dev/urandom | head -c' 17 | -------------------------------------------------------------------------------- /bin/thumb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Creates thumbnails for a directory of JPEG pictures. 5 | # 6 | # Usage: ./thumb.sh folder [size] 7 | # 8 | # Requires the ImageMagick convert binary to be installed. 9 | # 10 | 11 | set -e 12 | 13 | cd "$1" 14 | mkdir -p thumb 15 | 16 | TARGET_SIZE=${2:-120} 17 | SUBSAMPLE_SIZE=$((TARGET_SIZE*2)) 18 | 19 | for image in *.jpg; do 20 | if test -f "thumb/$image"; then continue; fi 21 | convert \ 22 | -define jpeg:size="${SUBSAMPLE_SIZE}x${SUBSAMPLE_SIZE}" "$image" \ 23 | -thumbnail "${TARGET_SIZE}x${TARGET_SIZE}^" \ 24 | -gravity center \ 25 | -extent "${TARGET_SIZE}x${TARGET_SIZE}" \ 26 | "thumb/$image" 27 | done 28 | -------------------------------------------------------------------------------- /bin/add-movie-subtitles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Combines a given MP4 video file with a subtitles file in the same directory. 5 | # The subtitles file must have the same file name, but with ".srt" extension. 6 | # 7 | # The language of the audio and subtitles track can be provided as optional 8 | # second argument as ISO 639-2 language code (default is "eng"). 9 | # 10 | # Usage: ./add-movie-subtitles.sh movie.mp4 [LANG] 11 | # 12 | # Requires FFmpeg to be installed. 13 | # 14 | 15 | set -e 16 | 17 | NAME=${1%.*} 18 | LANG=${2:-eng} 19 | 20 | file_exists () { 21 | if [ ! -e "$1" ]; then 22 | echo "File not found: \"$1\"" >&2 23 | return 1 24 | fi 25 | } 26 | 27 | file_exists "$NAME".mp4 28 | file_exists "$NAME".srt 29 | 30 | ffmpeg \ 31 | -i "$NAME".mp4 \ 32 | -i "$NAME".srt \ 33 | -c copy \ 34 | -c:s mov_text \ 35 | -metadata:s:s:0 language="$LANG" \ 36 | -metadata:s:a:0 language="$LANG" \ 37 | "$NAME [$LANG subtitles]".mp4 38 | -------------------------------------------------------------------------------- /bin/nfs-exports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Configures the given NFS export directories. 5 | # Also sets nfs.server.mount.require_resv_port option to 0. 6 | # 7 | # Requires sed, tee, nfsd. 8 | # 9 | # Usage: ./nfs-exports.sh [dir ...] 10 | # 11 | 12 | set -e 13 | 14 | MARKER='### nfs exports' 15 | START="$MARKER start ###" 16 | END="$MARKER end ###" 17 | NL=' 18 | ' 19 | MAPALL="$(id -u):$(id -g)" 20 | NFS_OPT=nfs.server.mount.require_resv_port 21 | 22 | NFS_CONF=$(sed "/^$NFS_OPT.*/d" /etc/nfs.conf) 23 | EXPORTS=$(sed "/$START/,/$END/d" /etc/exports) 24 | 25 | NFS_CONF="$NFS_CONF${NL}nfs.server.mount.require_resv_port=0" 26 | 27 | if [ "$#" -gt 0 ]; then 28 | EXPORTS="$EXPORTS${NL}$START" 29 | for DIR in "$@"; do 30 | DIR=$(cd "$DIR" && pwd) 31 | EXPORTS="$EXPORTS${NL}$DIR -alldirs -mapall=$MAPALL 127.0.0.1" 32 | done 33 | EXPORTS="$EXPORTS${NL}$END" 34 | fi 35 | 36 | echo "$NFS_CONF" | sed '/./,$!d' | sudo tee /etc/nfs.conf > /dev/null 37 | echo "$EXPORTS" | sed '/./,$!d' | sudo tee /etc/exports > /dev/null 38 | sudo nfsd checkexports 39 | sudo nfsd restart 40 | -------------------------------------------------------------------------------- /bin/reload-browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Reloads the active tab of the given browser (defaults to Chrome). 5 | # Keeps the browser window in the background (Chrome/Safari only). 6 | # Can optionally execute a given command before reloading the browser tab. 7 | # Browser reloading is supported on MacOS only for now. 8 | # 9 | # Usage: ./reload-browser.sh [chrome|safari|firefox] -- [command args...] 10 | # 11 | 12 | set -e 13 | 14 | RELOAD_CHROME='tell application "Google Chrome" 15 | reload active tab of window 1 16 | end tell' 17 | 18 | RELOAD_SAFARI='tell application "Safari" 19 | set URL of document 1 to (URL of document 1) 20 | end tell' 21 | 22 | RELOAD_FIREFOX='activate application "Firefox" 23 | tell application "System Events" to keystroke "r" using command down' 24 | 25 | case "$1" in 26 | firefox) OSASCRIPT=$RELOAD_FIREFOX;; 27 | safari) OSASCRIPT=$RELOAD_SAFARI;; 28 | *) OSASCRIPT=$RELOAD_CHROME;; 29 | esac 30 | 31 | if shift; then 32 | [ "$1" = "--" ] && shift 33 | "$@" 34 | fi 35 | 36 | if command -v osascript > /dev/null 2>&1; then 37 | exec osascript -e "$OSASCRIPT" 38 | fi 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2015 Sebastian Tschan, https://blueimp.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bin/buildkite-display-inline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Displays Buildkite artifacts inline. 5 | # Searches the given directory for files with the given extension. 6 | # Displays images (jpg|jpeg|gif|png) inline and other files as links. 7 | # Displays the inlines images/links in sorted order. 8 | # 9 | # See also: 10 | # https://buildkite.com/docs/pipelines/links-and-images-in-log-output 11 | # 12 | # Usage: ./buildkite-display-inline.sh directory extension 13 | # 14 | # Example: ./buildkite-display-inline.sh reports/screenshots png 15 | # 16 | 17 | set -e 18 | 19 | inline_link() { 20 | LINK="url='$1'" 21 | if [ -n "$2" ]; then 22 | LINK="$LINK;content='$2'" 23 | fi 24 | printf '\033]1339;%s\a\n' "$LINK" 25 | } 26 | 27 | inline_image() { 28 | printf '\033]1338;url=%s;alt=%s\a\n' "$1" "$2" 29 | } 30 | 31 | DIR=$1 32 | EXT=$2 33 | 34 | if [ ! -d "$DIR" ]; then exit; fi 35 | 36 | # If the current dir is not the git root, add a prefix to the artifact URLs: 37 | GIT_ROOT=$(git rev-parse --show-toplevel) 38 | if [ "$GIT_ROOT" != "$PWD" ]; then 39 | PREFIX=${PWD#$GIT_ROOT/}/ 40 | else 41 | PREFIX= 42 | fi 43 | 44 | # shellcheck disable=SC2039 45 | find "$DIR" -name "*.$EXT" -print0 | sort -z | while IFS= read -r -d '' FILE; do 46 | TITLE=$(basename "$FILE" ".$EXT") 47 | inline_link "artifact://$PREFIX$FILE" "$TITLE" 48 | case "$EXT" in 49 | jpg|jpeg|gif|png) inline_image "artifact://$PREFIX$FILE" "$TITLE";; 50 | esac 51 | done 52 | -------------------------------------------------------------------------------- /bin/aws-ec2-private-ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Returns the private IP of the EC2 instance with the given tag or ID. 5 | # By default the Name tag is queried. 6 | # A different tag key can be provided via --tag option. 7 | # If the --id flag is set, the value is used to query for the instance ID. 8 | # 9 | # For multiple instances with the same tag, an index after the value indicates 10 | # for which instance the private IP address should be returned. 11 | # e.g. "name" and "name0" return the first, "name1" the second, etc. 12 | # 13 | # Usage: ./aws-ec2-private-ip.sh [--tag key | --id] value[index] 14 | # 15 | # Example `~/.ssh/config` for an SSH Bastion setup: 16 | # 17 | # ``` 18 | # Host bastion 19 | # User ec2-user 20 | # HostName bastion.example.org 21 | # 22 | # Host apple* banana* orange* 23 | # User ec2-user 24 | # ProxyCommand ssh -W $(/path/to/aws-ec2-private-ip.sh '%h'):%p bastion 25 | # ``` 26 | # 27 | # With this config, each host can be connected to via their "name[index]". 28 | # e.g. `ssh banana2` 29 | # 30 | 31 | set -e 32 | 33 | if [ "$1" = --id ]; then 34 | set -- --instance-ids "$2" 35 | INDEX=1 36 | else 37 | if [ "$1" = --tag ]; then 38 | TAG=$2 39 | shift 2 40 | else 41 | TAG=Name 42 | fi 43 | VALUE=$(echo "$1" | sed 's/[0-9]$//g') 44 | INDEX=$((${1##$VALUE}+1)) 45 | set -- --filters "Name=tag:$TAG,Values=$VALUE" 46 | fi 47 | 48 | aws ec2 describe-instances \ 49 | --no-paginate \ 50 | --output text --query 'Reservations[*].Instances[*].PrivateIpAddress' \ 51 | "$@" | 52 | awk "NR==$INDEX" 53 | -------------------------------------------------------------------------------- /bin/replace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Global search and replace with variable arguments. 5 | # Takes input from STDIN and prints to STDOUT. 6 | # Supports multiline replacement strings. 7 | # 8 | # Usage: echo "$DATA" | ./replace.sh [-r] search_term replacement_string 9 | # 10 | # If the "-r" option is given, the search term is interpreted as 11 | # POSIX Basic Regular Expression. 12 | # Otherwise the search term is interpreted as literal string. 13 | # 14 | # Copyright 2015, Sebastian Tschan 15 | # https://blueimp.net 16 | # 17 | # Licensed under the MIT license: 18 | # https://opensource.org/licenses/MIT 19 | # 20 | 21 | # Global search and replace with the given pattern and replacement arguments: 22 | gsub() { 23 | # In sed replacement strings, slash, backslash and ampersand must be escaped. 24 | # Multiline strings are allowed, but must escape newlines with a backslash. 25 | # Therefore, the last sed sub call adds a backslash to all but the last line: 26 | sed "s/$1/$(echo "$2" | sed 's/[/\&]/\\&/g;$!s/$/\\/g')/g" 27 | } 28 | 29 | # Global search and replace with the given search and replacement strings: 30 | replace() { 31 | # In sed search patterns, the following characters have a special meaning: 32 | # The opening square bracket, slash, backslash, star and the dot. 33 | # Additionaly, the circumflex at the start and the dollar-sign at the end. 34 | # Therefore, we escape those characters in the given search string: 35 | gsub "$(echo "$1" | sed 's/[[/\*.]/\\&/g;s/^^/\\&/;s/$$/\\&/')" "$2" 36 | } 37 | 38 | if [ $# = 3 ] && [ "$1" = '-r' ]; then 39 | shift 40 | gsub "$@" 41 | else 42 | replace "$@" 43 | fi 44 | -------------------------------------------------------------------------------- /bin/docker-image-exists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Checks if a given docker image exists. 5 | # 6 | # Usage: ./docker-image-exists.sh image[:tag] 7 | # 8 | # Requires curl and jq to be installed. 9 | # 10 | # Copyright 2016, Sebastian Tschan 11 | # https://blueimp.net 12 | # 13 | # Licensed under the MIT license: 14 | # https://opensource.org/licenses/MIT 15 | # 16 | 17 | # Exit immediately if a command exits with a non-zero status: 18 | set -e 19 | 20 | if [ -z "$1" ]; then 21 | echo 'Usage: ./docker-image-exists.sh image[:tag]' >&2 22 | exit 1 23 | fi 24 | 25 | # Parse aguments: 26 | IMAGE="${1%:*}" 27 | TAG="${1##*:}" 28 | if [ "$IMAGE" = "$TAG" ]; then 29 | TAG=latest 30 | fi 31 | 32 | # Retrieve Docker Basic Authentication token: 33 | CREDS_STORE="$(jq -r '.credsStore' "$HOME/.docker/config.json")" 34 | if [ "$CREDS_STORE" = null ]; then 35 | BASIC_AUTH="$(jq -r '.auths["https://index.docker.io/v1/"].auth' \ 36 | "$HOME/.docker/config.json")" 37 | else 38 | BASIC_AUTH=$(echo https://index.docker.io/v1/ | 39 | docker-credential-"$CREDS_STORE" get | 40 | jq -j '"\(.Username):\(.Secret)"' | 41 | base64) 42 | fi 43 | 44 | # Define Docker access scope: 45 | SCOPE="repository:$IMAGE:pull" 46 | 47 | # Define the Docker token and registry URLs: 48 | TOKEN_URL="https://auth.docker.io/token?service=registry.docker.io&scope=$SCOPE" 49 | REGISTRY_URL="https://registry-1.docker.io/v2/$IMAGE/manifests/$TAG" 50 | 51 | # Retrieve the access token: 52 | TOKEN="$(curl -sSL -H "Authorization: Basic $BASIC_AUTH" "$TOKEN_URL" | 53 | jq -r '.token')" 54 | 55 | # Check if the given image tag exists: 56 | curl -fsSLI -o /dev/null -H "Authorization: Bearer $TOKEN" "$REGISTRY_URL" 57 | -------------------------------------------------------------------------------- /bin/cert-expiration-dates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Prints the expirations dates for the given certificate files. 5 | # 6 | # Requires openssl to be installed. 7 | # 8 | # Usage: ./cert-expiration-dates.sh cert1 [cert2 ...] 9 | # 10 | # Highlights certs expiring in 28 days in yellow warning color. 11 | # Highlights certs expiring in 14 days in red warning color. 12 | # 13 | # Returns exit code 1 if any certs will be expired in the next 14 days. 14 | # Returns exit code 0 otherwise. 15 | # 16 | # Copyright 2017, Sebastian Tschan 17 | # https://blueimp.net 18 | # 19 | # Licensed under the MIT license: 20 | # https://opensource.org/licenses/MIT 21 | # 22 | 23 | set -e 24 | 25 | get_padding() { 26 | padding=0 27 | for cert; do 28 | if [ ${#cert} -gt $padding ]; then 29 | padding=${#cert}; 30 | fi 31 | done 32 | echo "$padding" 33 | } 34 | 35 | expires_in_days() { 36 | ! openssl x509 -checkend $((60*60*24*$2)) -noout -in "$1" 37 | } 38 | 39 | print_expiration_date() { 40 | openssl x509 -enddate -noout -in "$1" | sed 's/notAfter=//' 41 | } 42 | 43 | print_expiration_dates() { 44 | for cert; do 45 | if expires_in_days "$cert" 14; then 46 | STATUS=1 47 | color="${c031}" 48 | elif expires_in_days "$cert" 28; then 49 | color="${c033}" 50 | else 51 | color="${c032}" 52 | fi 53 | printf "${c036}%s${c0} %s expires on $color%s${c0}\n" "$cert" \ 54 | "$(printf "%-$((PADDING-${#cert}))s" | tr ' ' '-')" \ 55 | "$(print_expiration_date "$cert")" 56 | done 57 | } 58 | 59 | # Color codes: 60 | c031='\033[0;31m' # red 61 | c032='\033[0;32m' # green 62 | c033='\033[0;33m' # yellow 63 | c036='\033[0;36m' # cyan 64 | c0='\033[0m' # no color 65 | 66 | PADDING="$(get_padding "$@")" 67 | 68 | STATUS=0 69 | 70 | print_expiration_dates "$@" 71 | 72 | exit $STATUS 73 | -------------------------------------------------------------------------------- /bin/apply-commit-times.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Sets the modification times of the given files and the files in the given 5 | # directories to their respective git commit timestamps. 6 | # 7 | # Usage: ./apply-commit-times.sh directory|file [...] 8 | # 9 | # Copyright 2017, Sebastian Tschan 10 | # https://blueimp.net 11 | # 12 | # Licensed under the MIT license: 13 | # https://opensource.org/licenses/MIT 14 | # 15 | 16 | set -e 17 | 18 | # Set the timezone to UTC so `git log` and `touch` do not diverge: 19 | export TZ=UTC0 20 | 21 | apply_commit_time() { 22 | # Extract the commit date for the file in `touch -t` format (CCYYMMDDhhmm.ss): 23 | timestamp=$(git log -1 --format=%cd --date=format-local:%Y%m%d%H%M.%S -- "$1") 24 | if [ -n "$timestamp" ]; then 25 | # Set the modification time of the given file to the commit timestamp: 26 | touch -t "$timestamp" "$1" 27 | fi 28 | } 29 | 30 | # Check if the shell supports the "-d" option for the `read` built-in: 31 | # shellcheck disable=SC2039 32 | if printf '%s\0' 1 2 | read -r -d '' 2>/dev/null; then 33 | iterate() { 34 | # Disable the internal field separator (IFS) and iterate over the null byte 35 | # separated file paths using the "-d" option of the `read` built-in: 36 | while IFS= read -r -d '' FILE; do apply_commit_time "$FILE"; done 37 | } 38 | else 39 | iterate() { 40 | # Transform the null byte separated files paths into command-line arguments 41 | # to the script itself via `xargs`. 42 | # The system-defined command-line arguments constraints will limit the 43 | # number of files that can be processed for a given directory, which should 44 | # be below the number defined via "-n" option for xargs: 45 | xargs -0 -n 100000 "$0" 46 | } 47 | fi 48 | 49 | while [ $# -gt 0 ]; do 50 | # Is the argument a directory path? 51 | if [ -d "$1" ]; then 52 | # The "-z" option of `git ls-tree` outputs null byte separated file paths: 53 | git ls-tree -r -z --name-only HEAD -- "$1" | iterate 54 | # Else is the argument a path to a readable file? 55 | elif [ -r "$1" ]; then 56 | apply_commit_time "$1" 57 | fi 58 | shift 59 | done 60 | -------------------------------------------------------------------------------- /bin/forward-ports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Adds TCP/UDP port forwarding rules to the pf firewall (MacOS/BSD). 5 | # 6 | # Adds rules for both TCP and UDP in addition to those from /etc/pf.conf. 7 | # Requires an existing rdr-anchor entry in /etc/pf.conf. 8 | # Only adds rules temporarily, without changing any files. 9 | # 10 | # Usage: ./forward-ports.sh [[nic:]port=[ip:]port [...]] 11 | # 12 | # If no network interface is given, forwards from all interfaces. 13 | # If no IP is given, forwards to 127.0.0.1. 14 | # If no port forwarding rule is given, resets to the rules from /etc/pf.conf. 15 | # 16 | # e.g. forwarding ports 80 and 443 on network interface en0 to ports 8080 and 17 | # 8443 on localhost respectively: 18 | # ./forward-ports.sh en0:80=8080 en0:443=8443 19 | # 20 | # Copyright 2019, Sebastian Tschan 21 | # https://blueimp.net 22 | # 23 | # Licensed under the MIT license: 24 | # https://opensource.org/licenses/MIT 25 | # 26 | 27 | set -e 28 | 29 | RULES= 30 | NEWLINE=' 31 | ' 32 | 33 | print_usage_exit() { 34 | if [ -n "$RULES" ]; then 35 | printf '\nError in custom rules:\n%s\n' "$RULES" >&2 36 | fi 37 | echo "Usage: $0 [[nic:]port=[ip:]port [...]]" >&2 38 | exit 1 39 | } 40 | 41 | print_nat_rules() { 42 | echo 43 | echo 'Loaded NAT rules:' 44 | sudo pfctl -s nat 2>/dev/null 45 | echo 46 | } 47 | 48 | # Print usage and exit if option arguments like "-h" are used: 49 | if [ "${1#-}" != "$1" ]; then print_usage_exit; fi 50 | 51 | while test $# -gt 0; do 52 | # Separate the from=to parts: 53 | from=${1%=*} 54 | to=${1#*=} 55 | # If from part has a nic defined, extract it, else forward from all: 56 | case "$from" in 57 | *:*) nic="on ${from%:*}";; 58 | *) nic=;; 59 | esac 60 | # Extract the port to forward from: 61 | from_port=${from##*:} 62 | # If to part has an IP defined, extract it, else forward to 127.0.0.1: 63 | case "$to" in 64 | *:*) to_ip=${to%:*};; 65 | *) to_ip=127.0.0.1;; 66 | esac 67 | # Extract the port to forward to: 68 | to_port=${to##*:} 69 | # Create the packet filter (pf) forwarding rule for both TCP and UDP: 70 | rule=$( 71 | printf \ 72 | 'rdr pass %s inet proto %s from any to any port %s -> %s port %s' \ 73 | "$nic" '{tcp udp}' "$from_port" "$to_ip" "$to_port" 74 | ) 75 | # Add it to the list of rules: 76 | RULES="$RULES$rule$NEWLINE" 77 | shift 78 | done 79 | 80 | # Add the rules after the line matching "rdr-anchor" in /etc/pf.conf, print the 81 | # combined rules to STDOUT and load the rules into pf from STDIN. 82 | # Finally, display the loaded NAT rules or print the script usage on failure: 83 | # shellcheck disable=SC2015 84 | printf %s "$RULES" | sed -e '/rdr-anchor/r /dev/stdin' /etc/pf.conf | 85 | sudo pfctl -Ef - 2>/dev/null && print_nat_rules || print_usage_exit 86 | -------------------------------------------------------------------------------- /bin/log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Executes the given command and logs the output. 5 | # Adds a datetime and PID prefix in front of each output line. 6 | # Also logs the effective user and given command line. 7 | # 8 | # Usage: ./log.sh command [args...] 9 | # 10 | # The location of the log output can be defined 11 | # with the following environment variable: 12 | # LOGFILE='/var/log/out.log' 13 | # 14 | # The error log can also be piped into a separate file 15 | # defined by the following environment variable: 16 | # ERRFILE='/var/log/err.log' 17 | # 18 | # The date output formatting can be defined 19 | # with the following environment variable: 20 | # DATECMD='date -u +%Y-%m-%dT%H:%M:%SZ' 21 | # 22 | # The PID printf format can be defined 23 | # with the following environment variable: 24 | # PIDFORM='(%05d)' 25 | # 26 | # Copyright 2015, Sebastian Tschan 27 | # https://blueimp.net 28 | # 29 | # Licensed under the MIT license: 30 | # https://opensource.org/licenses/MIT 31 | # 32 | 33 | # Define default values: 34 | [ -z "$ERRFILE" ] && ERRFILE="$LOGFILE" 35 | [ -z "$DATECMD" ] && DATECMD='date -u +%Y-%m-%dT%H:%M:%SZ' 36 | [ -z "$PIDFORM" ] && PIDFORM='(%05d)' 37 | 38 | # Combines the given arguments with a datetime and PID prefix: 39 | format() { 40 | # shellcheck disable=SC2059 41 | echo "$($DATECMD) $(printf "$PIDFORM" $$) [$1] $2" 42 | } 43 | 44 | # Logs to stdout: 45 | out_log() { 46 | format "$@" 47 | } 48 | 49 | # Logs to stderr: 50 | err_log() { 51 | format "$@" >&2 52 | } 53 | 54 | # Logs to a file: 55 | out_file_log() { 56 | format "$@" >> "$LOGFILE" 57 | } 58 | 59 | # Logs to the errors file: 60 | err_file_log() { 61 | format "$@" >> "$ERRFILE" 62 | } 63 | 64 | # Returns the defined log output: 65 | get_log() { 66 | printf %s "${1:-out}" 67 | if [ "$1" = 'err' ] && [ -n "$ERRFILE" ] || [ -n "$LOGFILE" ]; then 68 | printf _file 69 | fi 70 | printf _log 71 | } 72 | 73 | # Processes stdin and logs each line: 74 | process() { 75 | log=$(get_log "$1") 76 | while read -r line; do 77 | $log "$1" "$line" 78 | done 79 | } 80 | 81 | # Returns a string with the quoted arguments: 82 | quote() { 83 | args= 84 | for arg; do 85 | # Escape single quotes: 86 | arg="$(echo "$arg" | sed "s/'/'\\\\''/g")" 87 | case "$arg" in 88 | # Quote arguments containing characters not in the whitelist: 89 | *[!a-zA-Z0-9_-]*) 90 | args="$args'$arg' ";; 91 | *) 92 | args="$args$arg ";; 93 | esac 94 | done 95 | echo "$args" 96 | } 97 | 98 | # Log the effective user: 99 | $(get_log) usr "$(whoami)" 100 | 101 | # Log the command: 102 | $(get_log) cmd "$(quote "$@")" 103 | 104 | # Set line buffered mode if the stdbuf command is available: 105 | if [ $# -gt 0 ] && command -v stdbuf > /dev/null 2>&1; then 106 | set -- stdbuf -oL -eL "$@" 107 | fi 108 | 109 | # Execute the command and log stdout and stderr: 110 | { "$@" 2>&3 | process out; } 3>&1 1>&2 | process err 111 | -------------------------------------------------------------------------------- /bin/wait-for-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Waits for the given file(s) to exist before executing a given command. 5 | # Tests for existance by using `test -e`, which also allows to test for 6 | # directories in addition to files. 7 | # 8 | # Usage: 9 | # ./wait-for-files.sh [-q] [-t seconds] [file] [...] [-- command args...] 10 | # 11 | # The script accepts multiple files as arguments or defined as WAIT_FOR_FILES 12 | # environment variable, separating the file paths via colons. 13 | # 14 | # The status output can be made quiet by adding the `-q` argument or by setting 15 | # the environment variable WAIT_FOR_FILES_QUIET to `1`. 16 | # 17 | # The default timeout of 10 seconds can be changed via `-t seconds` argument or 18 | # by setting the WAIT_FOR_FILES_TIMEOUT environment variable to the desired 19 | # number of seconds. 20 | # 21 | # The command defined after the `--` argument separator will be executed if all 22 | # the given files exist. 23 | # 24 | # Copyright 2019, Sebastian Tschan 25 | # https://blueimp.net 26 | # 27 | # Licensed under the MIT license: 28 | # https://opensource.org/licenses/MIT 29 | # 30 | 31 | set -e 32 | 33 | is_integer() { 34 | test "$1" -eq "$1" 2> /dev/null 35 | } 36 | 37 | set_timeout() { 38 | if ! is_integer "$1"; then 39 | printf 'Error: "%s" is not a valid timeout value.\n' "$1" >&2 40 | return 1 41 | fi 42 | TIMEOUT="$1" 43 | } 44 | 45 | quiet_echo() { 46 | if [ "$QUIET" -ne 1 ]; then echo "$@" >&2; fi 47 | } 48 | 49 | wait_for_file() { 50 | FILE=$1 51 | if [ "$QUIET" -ne 1 ]; then 52 | printf "Waiting for file: %-${PADDING}s ... " "$1" >&2 53 | fi 54 | TIME_LIMIT=$(($(date +%s)+TIMEOUT)) 55 | while ! test -e "$FILE"; do 56 | if [ "$(date +%s)" -ge "$TIME_LIMIT" ]; then 57 | quiet_echo timeout 58 | return 1 59 | fi 60 | sleep 1 61 | done 62 | quiet_echo ok 63 | } 64 | 65 | wait_for_files() { 66 | if [ -z "$1" ]; then 67 | return 68 | fi 69 | ORIGINAL_IFS=$IFS 70 | IFS=: 71 | # shellcheck disable=SC2086 72 | set -- $1 73 | IFS=$ORIGINAL_IFS 74 | for FILE; do 75 | wait_for_file "$FILE" 76 | done 77 | } 78 | 79 | set_padding() { 80 | PADDING=0 81 | ORIGINAL_IFS=$IFS 82 | IFS=: 83 | # shellcheck disable=SC2086 84 | set -- $WAIT_FOR_FILES "$@" 85 | IFS=$ORIGINAL_IFS 86 | while [ $# != 0 ]; do 87 | case "$1" in 88 | -t) shift 2;; 89 | -q) break;; 90 | --) break;; 91 | *) test ${#1} -gt $PADDING && PADDING=${#1}; shift;; 92 | esac 93 | done 94 | } 95 | 96 | QUIET=${WAIT_FOR_FILES_QUIET:-0} 97 | set_timeout "${WAIT_FOR_FILES_TIMEOUT:-10}" 98 | 99 | if [ "$QUIET" -ne 1 ]; then 100 | set_padding "$@" 101 | fi 102 | 103 | while [ $# != 0 ]; do 104 | case "$1" in 105 | -t) set_timeout "$2"; shift 2;; 106 | -q) QUIET=1; shift;; 107 | --) shift; break;; 108 | *) wait_for_file "$1"; shift;; 109 | esac 110 | done 111 | 112 | wait_for_files "$WAIT_FOR_FILES" 113 | 114 | exec "$@" 115 | -------------------------------------------------------------------------------- /bin/wait-for-hostnames.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Waits for the given hostname(s) to resolve to an IP before executing a given 5 | # command. 6 | # Resolves using `getent` (Linux/BSD) if available or `dscacheutil` (MacOS). 7 | # 8 | # Usage: 9 | # ./wait-for-hostnames.sh [-q] [-t seconds] [hostname] [...] [-- cmd args...] 10 | # 11 | # The script accepts multiple hostnames as arguments or defined as 12 | # WAIT_FOR_HOSTNAMES environment variable, separating the hostnames via spaces. 13 | # 14 | # The status output can be made quiet by adding the `-q` argument or by setting 15 | # the environment variable WAIT_FOR_HOSTNAMES_QUIET to `1`. 16 | # 17 | # The default timeout of 10 seconds can be changed via `-t seconds` argument or 18 | # by setting the WAIT_FOR_HOSTNAMES_TIMEOUT environment variable to the desired 19 | # number of seconds. 20 | # 21 | # The command defined after the `--` argument separator will be executed if all 22 | # the given hostnames can be resolved. 23 | # 24 | # Copyright 2019, Sebastian Tschan 25 | # https://blueimp.net 26 | # 27 | # Licensed under the MIT license: 28 | # https://opensource.org/licenses/MIT 29 | # 30 | 31 | set -e 32 | 33 | if command -v getent > /dev/null 2>&1; then 34 | resolve_host() { 35 | getent hosts "$1" 36 | } 37 | else 38 | resolve_host() { 39 | test -n "$(dscacheutil -q host -a name "$1")" 40 | } 41 | fi 42 | 43 | is_integer() { 44 | test "$1" -eq "$1" 2> /dev/null 45 | } 46 | 47 | set_timeout() { 48 | if ! is_integer "$1"; then 49 | printf 'Error: "%s" is not a valid timeout value.\n' "$1" >&2 50 | return 1 51 | fi 52 | TIMEOUT="$1" 53 | } 54 | 55 | quiet_echo() { 56 | if [ "$QUIET" -ne 1 ]; then echo "$@" >&2; fi 57 | } 58 | 59 | wait_for_host() { 60 | HOST=$1 61 | if [ "$QUIET" -ne 1 ]; then 62 | printf "Waiting for hostname: %-${PADDING}s ... " "$1" >&2 63 | fi 64 | TIME_LIMIT=$(($(date +%s)+TIMEOUT)) 65 | while ! OUTPUT="$(resolve_host "$HOST" 2>&1)"; do 66 | if [ "$(date +%s)" -ge "$TIME_LIMIT" ]; then 67 | quiet_echo timeout 68 | if [ -n "$OUTPUT" ]; then 69 | quiet_echo "$OUTPUT" 70 | fi 71 | return 1 72 | fi 73 | sleep 1 74 | done 75 | quiet_echo ok 76 | } 77 | 78 | set_padding() { 79 | PADDING=0 80 | while [ $# != 0 ]; do 81 | case "$1" in 82 | -t) shift 2;; 83 | -q) break;; 84 | --) break;; 85 | *) test ${#1} -gt $PADDING && PADDING=${#1}; shift;; 86 | esac 87 | done 88 | } 89 | 90 | QUIET=${WAIT_FOR_HOSTNAMES_QUIET:-0} 91 | set_timeout "${WAIT_FOR_HOSTNAMES_TIMEOUT:-10}" 92 | 93 | if [ "$QUIET" -ne 1 ]; then 94 | # shellcheck disable=SC2086 95 | set_padding $WAIT_FOR_HOSTNAMES "$@" 96 | fi 97 | 98 | while [ $# != 0 ]; do 99 | case "$1" in 100 | -t) set_timeout "$2"; shift 2;; 101 | -q) QUIET=1; shift;; 102 | --) shift; break;; 103 | *) wait_for_host "$1"; shift;; 104 | esac 105 | done 106 | 107 | for HOST in $WAIT_FOR_HOSTNAMES; do 108 | wait_for_host "$HOST" 109 | done 110 | 111 | exec "$@" 112 | -------------------------------------------------------------------------------- /bin/wait-for-hosts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Waits for the given host(s) to be available before executing a given command. 5 | # Tests for availability by using netcat to connect to the hosts via TCP. 6 | # 7 | # Usage: 8 | # ./wait-for-hosts.sh [-q] [-t seconds] [host:port] [...] [-- command args...] 9 | # 10 | # The script accepts multiple `host:port` combinations as arguments or defined 11 | # as WAIT_FOR_HOSTS environment variable, separating the `host:port` 12 | # combinations via spaces. 13 | # 14 | # The status output can be made quiet by adding the `-q` argument or by setting 15 | # the environment variable WAIT_FOR_HOSTS_QUIET to `1`. 16 | # 17 | # The default timeout of 10 seconds can be changed via `-t seconds` argument or 18 | # by setting the WAIT_FOR_HOSTS_TIMEOUT environment variable to the desired 19 | # number of seconds. 20 | # 21 | # The command defined after the `--` argument separator will be executed if all 22 | # the given hosts are reachable. 23 | # 24 | # Copyright 2016, Sebastian Tschan 25 | # https://blueimp.net 26 | # 27 | # Licensed under the MIT license: 28 | # https://opensource.org/licenses/MIT 29 | # 30 | 31 | set -e 32 | 33 | is_integer() { 34 | test "$1" -eq "$1" 2> /dev/null 35 | } 36 | 37 | set_timeout() { 38 | if ! is_integer "$1"; then 39 | printf 'Error: "%s" is not a valid timeout value.\n' "$1" >&2 40 | return 1 41 | fi 42 | TIMEOUT="$1" 43 | } 44 | 45 | connect_to_service() { 46 | nc -w 1 -z "$1" "$2" 47 | } 48 | 49 | quiet_echo() { 50 | if [ "$QUIET" -ne 1 ]; then echo "$@" >&2; fi 51 | } 52 | 53 | wait_for_host() { 54 | HOST="${1%:*}" 55 | PORT="${1#*:}" 56 | if ! is_integer "$PORT"; then 57 | printf 'Error: "%s" is not a valid host:port combination.\n' "$1" >&2 58 | return 1 59 | fi 60 | if [ "$QUIET" -ne 1 ]; then 61 | printf "Waiting for host: %-${PADDING}s ... " "$1" >&2 62 | fi 63 | TIME_LIMIT=$(($(date +%s)+TIMEOUT)) 64 | while ! OUTPUT="$(connect_to_service "$HOST" "$PORT" 2>&1)"; do 65 | if [ "$(date +%s)" -ge "$TIME_LIMIT" ]; then 66 | quiet_echo timeout 67 | if [ -n "$OUTPUT" ]; then 68 | quiet_echo "$OUTPUT" 69 | fi 70 | return 1 71 | fi 72 | sleep 1 73 | done 74 | quiet_echo ok 75 | } 76 | 77 | set_padding() { 78 | PADDING=0 79 | while [ $# != 0 ]; do 80 | case "$1" in 81 | -t) shift 2;; 82 | -q) break;; 83 | --) break;; 84 | *) test ${#1} -gt $PADDING && PADDING=${#1}; shift;; 85 | esac 86 | done 87 | } 88 | 89 | QUIET=${WAIT_FOR_HOSTS_QUIET:-0} 90 | set_timeout "${WAIT_FOR_HOSTS_TIMEOUT:-10}" 91 | 92 | if [ "$QUIET" -ne 1 ]; then 93 | # shellcheck disable=SC2086 94 | set_padding $WAIT_FOR_HOSTS "$@" 95 | fi 96 | 97 | while [ $# != 0 ]; do 98 | case "$1" in 99 | -t) set_timeout "$2"; shift 2;; 100 | -q) QUIET=1; shift;; 101 | --) shift; break;; 102 | *) wait_for_host "$1"; shift;; 103 | esac 104 | done 105 | 106 | for HOST in $WAIT_FOR_HOSTS; do 107 | wait_for_host "$HOST" 108 | done 109 | 110 | exec "$@" 111 | -------------------------------------------------------------------------------- /bin/superd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Supervisor daemon to manage long running processes as a group. 5 | # Terminates all remaining child processes as soon as one child exits. 6 | # 7 | # Usage: ./superd.sh [config_file] 8 | # 9 | # The default superd configuration file is "/usr/local/etc/superd.conf". 10 | # An alternate configuration file can be provided as first argument. 11 | # To read the configuration from STDIN, the placeholder "-" can be used. 12 | # 13 | # Each line of the superd configuration file must have the following format: 14 | # command [args...] 15 | # Empty lines and lines starting with a hash (#) will be ignored. 16 | # Each command will be run by superd as a background process. 17 | # If one command terminates, all commands will be terminated. 18 | # 19 | # Copyright 2015, Sebastian Tschan 20 | # https://blueimp.net 21 | # 22 | # Licensed under the MIT license: 23 | # https://opensource.org/licenses/MIT 24 | # 25 | 26 | # The list of process IDs for the background processes: 27 | PIDS= 28 | 29 | # Runs the given command in a background process and stores the process ID: 30 | run() { 31 | "$@" & 32 | PIDS="$PIDS $!" 33 | } 34 | 35 | # Determines the config file: 36 | config() { 37 | case "$1" in 38 | -) echo /dev/stdin;; 39 | '') echo /usr/local/etc/superd.conf;; 40 | *) echo "$1";; 41 | esac 42 | } 43 | 44 | # Runs commands defined in the given config file: 45 | startup() { 46 | while read -r line; do 47 | # Skip empty lines and lines starting with a hash (#): 48 | [ -z "$line" ] || [ "${line#\#}" != "$line" ] && continue 49 | # Run the given command line: 50 | # shellcheck disable=SC2086 51 | run $line 52 | # Use the given config file as input: 53 | done < "$1" 54 | } 55 | 56 | # Returns all given processes and their descendants tree as flat list: 57 | collect() { 58 | for pid in "$@"; do 59 | printf ' %s' "$pid" 60 | # shellcheck disable=SC2046 61 | collect $(pgrep -P "$pid") 62 | done 63 | } 64 | 65 | # Terminates the given list of processes: 66 | terminate() { 67 | for pid in "$@"; do 68 | # Terminate the given process, ignore stdout and stderr output: 69 | kill "$pid" > /dev/null 2>&1 70 | # Wait for the process to stop: 71 | wait "$pid" 72 | done 73 | } 74 | 75 | # Initiates a shutdown by terminating the tree of child processes: 76 | shutdown() { 77 | # shellcheck disable=SC2046 78 | terminate $(collect $(pgrep -P $$)) 79 | } 80 | 81 | # Monitors the started background processes until one of them exits: 82 | monitor() { 83 | while true; do 84 | for pid in $PIDS; do 85 | # Return if the given process is not running: 86 | ! kill -s 0 "$pid" > /dev/null 2>&1 && return 87 | done 88 | sleep 1 89 | done 90 | } 91 | 92 | # Initiate a shutdown on SIGINT and SIGTERM: 93 | trap 'shutdown; exit' INT TERM 94 | 95 | # Start the commands with the given config: 96 | startup "$(config "$@")" || exit $? 97 | 98 | # Monitor the started background processes until one of them exits: 99 | monitor 100 | 101 | # Initiate a shutdown: 102 | shutdown 103 | -------------------------------------------------------------------------------- /bin/android-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Starts an Android virtual device with a writeable filesystem. 5 | # If the -hosts option is provided, replaces /etc/hosts on the device with the 6 | # given hosts file. 7 | # If the -return option is given, returns to the caller when the emulator is 8 | # ready, otherwise waits for the emulator process to stop. 9 | # If no emulator -avd option is given, starts the first AVD in the list. 10 | # If no existing AVD is available, creates a new one. 11 | # 12 | # Usage: ./android-emulator.sh [-hosts file] [-return] [-- emulator options] 13 | # 14 | # Copyright 2019, Sebastian Tschan 15 | # https://blueimp.net 16 | # 17 | # Licensed under the MIT license: 18 | # https://opensource.org/licenses/MIT 19 | # 20 | 21 | set -e 22 | 23 | DEVICE_ID='pixel' 24 | SYSTEM_IMAGE_REGEXP='system-images;android-[0-9]*;google_apis;x86\>' 25 | WRITABLE_SYSTEM_IMAGE='system-images;android-28;google_apis;x86' 26 | SDCARD='512M' 27 | 28 | if [ -z "$ANDROID_HOME" ]; then 29 | echo 'Error: ANDROID_HOME is not defined.' >&2 30 | exit 1 31 | fi 32 | 33 | adb() { 34 | "$ANDROID_HOME/platform-tools/adb" "$@" 35 | } 36 | 37 | emulator() { 38 | "$ANDROID_HOME/emulator/emulator" "$@" 39 | } 40 | 41 | avdmanager() { 42 | "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" "$@" 43 | } 44 | 45 | sdkmanager() { 46 | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "$@" 47 | } 48 | 49 | normalize() { 50 | echo "$1" | sed 's/[^a-z A-Z 0-9._-]/-/g' 51 | } 52 | 53 | get_avd() { 54 | emulator -list-avds | head -n 1 55 | } 56 | 57 | get_image() { 58 | if [ -n "$HOSTS_FILE" ]; then 59 | echo "$WRITABLE_SYSTEM_IMAGE" 60 | else 61 | sdkmanager --list | grep -o "$SYSTEM_IMAGE_REGEXP" | tail -1 62 | fi 63 | } 64 | 65 | download_image() { 66 | sdkmanager "$1" 67 | } 68 | 69 | create_avd() { 70 | echo 'Downloading system image ...' 71 | download_image "$1" 72 | echo 'System image downloaded.' 73 | echo 'Creating Android Virtual Device ...' 74 | avdmanager create avd \ 75 | --name "$(normalize "$DEVICE_ID-${1#*;}")" \ 76 | --package "$1" \ 77 | --device "$DEVICE_ID" \ 78 | --sdcard "$SDCARD" 79 | echo 'Virtual Device created.' 80 | } 81 | 82 | has_arg() { 83 | while test $# -gt 0; do 84 | test "$1" = "$ARG" && return 0 85 | shift 86 | done 87 | return 1 88 | } 89 | 90 | has_system_prop() { 91 | test "$(adb shell getprop "$1" | tr -d '\r')" = "$2" 92 | } 93 | 94 | wait_for_device() { 95 | echo 'Waiting for device to be ready ...' 96 | adb wait-for-device 97 | while ! has_system_prop sys.boot_completed 1; do 98 | sleep 1 99 | done 100 | echo 'Device ready.' 101 | } 102 | 103 | update_hosts_file() { 104 | adb root 105 | wait_for_device 106 | adb remount 107 | adb push "$1" /etc/hosts 108 | adb unroot 109 | wait_for_device 110 | } 111 | 112 | if [ "$1" = -hosts ]; then 113 | HOSTS_FILE=$2 114 | shift 2 115 | fi 116 | 117 | if [ "$1" = -return ]; then 118 | RETURN=true 119 | shift 120 | fi 121 | 122 | if [ "$1" = -- ]; then 123 | shift 124 | fi 125 | 126 | if ! ARG=-avd has_arg "$@"; then 127 | if [ -z "$(get_avd)" ]; then 128 | create_avd "$(get_image)" 129 | fi 130 | set -- -avd "$(get_avd)" "$@" 131 | fi 132 | 133 | if [ -n "$HOSTS_FILE" ]; then 134 | set -- -writable-system "$@" 135 | fi 136 | 137 | emulator "$@" & PID=$! 138 | 139 | wait_for_device 140 | 141 | if [ -n "$HOSTS_FILE" ]; then 142 | update_hosts_file "$HOSTS_FILE" 143 | fi 144 | 145 | if [ "$RETURN" = true ]; then 146 | exit 147 | fi 148 | 149 | wait "$PID" 150 | -------------------------------------------------------------------------------- /bin/aws-website-redirect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Creates an S3 bucket with a website redirect rule and adds a Route53 alias. 5 | # 6 | # Requires aws CLI to be installed. 7 | # Redirects to the www subdomain by default. 8 | # Always redirects to an https URL. 9 | # 10 | # Usage: ./aws-website-redirect.sh [--region region] hostname [redirect_host] 11 | # 12 | # Copyright 2017, Sebastian Tschan 13 | # https://blueimp.net 14 | # 15 | # Licensed under the MIT license: 16 | # https://opensource.org/licenses/MIT 17 | # 18 | 19 | set -e 20 | 21 | if [ "$1" = --region ]; then 22 | REGION="$2" 23 | shift 2 24 | else 25 | REGION=${AWS_DEFAULT_REGION:?} 26 | fi 27 | 28 | # Map the region to S3 website endpoint and S3 hosted zone ID: 29 | # http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_website_region_endpoints 30 | REGION_MAPPING=$(echo ' 31 | s3-website.us-east-2.amazonaws.com=Z2O1EMRO9K5GLX 32 | s3-website-us-east-1.amazonaws.com=Z3AQBSTGFYJSTF 33 | s3-website-us-west-1.amazonaws.com=Z2F56UZL2M1ACD 34 | s3-website-us-west-2.amazonaws.com=Z3BJ6K6RIION7M 35 | s3-website.ap-south-1.amazonaws.com=Z11RGJOFQNVJUP 36 | s3-website.ap-northeast-3.amazonaws.com=Z2YQB5RD63NC85 37 | s3-website.ap-northeast-2.amazonaws.com=Z3W03O7B5YMIYP 38 | s3-website-ap-southeast-1.amazonaws.com=Z3O0J2DXBE1FTB 39 | s3-website-ap-southeast-2.amazonaws.com=Z1WCIGYICN2BYD 40 | s3-website-ap-northeast-1.amazonaws.com=Z2M4EHUR26P7ZW 41 | s3-website.ca-central-1.amazonaws.com=Z1QDHH18159H29 42 | s3-website.eu-central-1.amazonaws.com=Z21DNDUVLTQW6Q 43 | s3-website-eu-west-1.amazonaws.com=Z1BKCTXD74EZPE 44 | s3-website.eu-west-2.amazonaws.com=Z3GKZC51ZF0DB4 45 | s3-website.eu-west-3.amazonaws.com=Z3R1K369G5AVDG 46 | s3-website.eu-north-1.amazonaws.com=Z3BAZG2TWCNX0D 47 | s3-website-sa-east-1.amazonaws.com=Z7KQH4QJS55SO 48 | ' | grep "$REGION") 49 | 50 | BUCKET_HOSTNAME=${REGION_MAPPING%=*} 51 | S3_HOSTED_ZONE_ID=${REGION_MAPPING#*=} 52 | 53 | HOSTNAME=${1:?} 54 | REDIRECT_HOSTNAME=${2:-www.$1} 55 | 56 | # Extract the root domain: 57 | TLD=${HOSTNAME##*.} 58 | SUBDOMAIN_PARTS=${HOSTNAME%.*.$TLD} 59 | ROOT_DOMAIN=${HOSTNAME#$SUBDOMAIN_PARTS.} 60 | 61 | WEBSITE_CONFIGURATION=$(printf '{ 62 | "RedirectAllRequestsTo": { 63 | "HostName": "%s", 64 | "Protocol": "https" 65 | } 66 | }' "$REDIRECT_HOSTNAME") 67 | 68 | CHANGE_BATCH=$(printf '{ 69 | "Changes": [ 70 | { 71 | "Action": "UPSERT", 72 | "ResourceRecordSet": { 73 | "Name": "%s", 74 | "Type": "A", 75 | "AliasTarget": { 76 | "DNSName": "%s", 77 | "HostedZoneId": "%s", 78 | "EvaluateTargetHealth": false 79 | } 80 | } 81 | } 82 | ] 83 | }' "$HOSTNAME" "$BUCKET_HOSTNAME" "$S3_HOSTED_ZONE_ID") 84 | 85 | create_bucket() { 86 | aws s3api create-bucket \ 87 | --bucket "$1" \ 88 | --region "$2" \ 89 | --create-bucket-configuration LocationConstraint="$2" 90 | } 91 | 92 | put_bucket_website() { 93 | aws s3api put-bucket-website \ 94 | --bucket "$1" \ 95 | --website-configuration "$2" 96 | } 97 | 98 | get_hosted_zone_id() { 99 | aws route53 list-hosted-zones-by-name --dns-name "$1" --max-items 1 \ 100 | --query "HostedZones[?Name == '$1.'].Id" --output text | 101 | sed s,/hostedzone/,, 102 | } 103 | 104 | change_resource_record_sets() { 105 | aws route53 change-resource-record-sets \ 106 | --hosted-zone-id "$1" \ 107 | --change-batch "$2" 108 | } 109 | 110 | if ! aws s3api head-bucket --bucket "$HOSTNAME" 2> /dev/null; then 111 | create_bucket "$HOSTNAME" "$REGION" 112 | fi 113 | 114 | put_bucket_website "$HOSTNAME" "$WEBSITE_CONFIGURATION" 115 | echo "$WEBSITE_CONFIGURATION" 116 | 117 | HOSTED_ZONE_ID=$(get_hosted_zone_id "$ROOT_DOMAIN") 118 | if [ -n "$HOSTED_ZONE_ID" ]; then 119 | change_resource_record_sets "$HOSTED_ZONE_ID" "$CHANGE_BATCH" 120 | fi 121 | -------------------------------------------------------------------------------- /bin/github-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Creates a GitHub release or pre-release for tagged commits. 5 | # Uploads the given files as release assets. 6 | # 7 | # Requires git, curl and jq. 8 | # 9 | # Usage: ./github-release.sh [--no-color] [FILE ...] 10 | # 11 | # The --no-color argument allows to disable the default color output. 12 | # 13 | 14 | set -e 15 | 16 | # Colorize the output by default: 17 | if [ "$1" != '--no-color' ]; then 18 | c031='\033[0;31m' # red 19 | c032='\033[0;32m' # green 20 | c033='\033[0;33m' # yellow 21 | c036='\033[0;36m' # cyan 22 | c0='\033[0m' # no color 23 | else 24 | shift 1 25 | c031= 26 | c032= 27 | c033= 28 | c036= 29 | c0= 30 | fi 31 | 32 | if [ -z "$GITHUB_TOKEN" ]; then 33 | printf "${c031}Error${c0}: Missing ${c033}%s${c0} environment variable.\\n" \ 34 | GITHUB_TOKEN >&2 35 | exit 1 36 | fi 37 | 38 | # Check if the current directory is a git repository: 39 | git -C . rev-parse 40 | 41 | ORIGIN_URL=$(git config --get remote.origin.url) 42 | GITHUB_ORG=$(echo "$ORIGIN_URL" | sed 's|.*:||;s|/.*$||') 43 | GITHUB_REPO=$(echo "$ORIGIN_URL" | sed 's|.*/||;s|\.[^\.]*$||') 44 | export GITHUB_ORG 45 | export GITHUB_REPO 46 | 47 | # Get the tag for the current commit: 48 | TAG="$(git describe --exact-match --tags 2> /dev/null || true)" 49 | 50 | if [ -z "$TAG" ]; then 51 | printf "${c033}%s${c0}: Not a tagged commit\\n" Warning 52 | exit 53 | fi 54 | 55 | # Check if this is a pre-release version (denoted by a hyphen): 56 | if [ "${TAG#*-}" != "$TAG" ]; then 57 | PRE=true 58 | else 59 | PRE=false 60 | fi 61 | 62 | RELEASE_TEMPLATE='{ 63 | "tag_name": "%s", 64 | "name": "%s", 65 | "prerelease": %s, 66 | "draft": %s 67 | }' 68 | 69 | create_draft_release() { 70 | # shellcheck disable=SC2059 71 | data="$(printf "$RELEASE_TEMPLATE" "$TAG" "$TAG" "$PRE" true)" 72 | if output=$(curl \ 73 | --silent \ 74 | --fail \ 75 | --request POST \ 76 | --header "Authorization: token $GITHUB_TOKEN" \ 77 | --header 'Content-Type: application/json' \ 78 | --data "$data" \ 79 | "https://api.github.com/repos/$GITHUB_ORG/$GITHUB_REPO/releases"); 80 | then 81 | RELEASE_ID=$(echo "$output" | jq -re '.id') 82 | UPLOAD_URL_TEMPLATE=$(echo "$output" | jq -re '.upload_url') 83 | fi 84 | } 85 | 86 | upload_release_asset() { 87 | mime_type=$(file -b --mime-type "$1") 88 | curl \ 89 | --silent \ 90 | --fail \ 91 | --request POST \ 92 | --header "Authorization: token $GITHUB_TOKEN" \ 93 | --header "Content-Type: $mime_type" \ 94 | --data-binary "@$1" \ 95 | "${UPLOAD_URL_TEMPLATE%\{*}?name=$(basename "$1")" \ 96 | > /dev/null 97 | } 98 | 99 | publish_release() { 100 | # shellcheck disable=SC2059 101 | data="$(printf "$RELEASE_TEMPLATE" "$TAG" "$TAG" "$PRE" false)" 102 | curl \ 103 | --silent \ 104 | --fail \ 105 | --request PATCH \ 106 | --header "Authorization: token $GITHUB_TOKEN" \ 107 | --header 'Content-Type: application/json' \ 108 | --data "$data" \ 109 | "https://api.github.com/repos/$GITHUB_ORG/$GITHUB_REPO/releases/$1" \ 110 | > /dev/null 111 | } 112 | 113 | printf "Creating draft release ${c036}%s${c0} ... " "$TAG" 114 | create_draft_release \ 115 | && echo "${c032}done${c0}" || echo "${c031}fail${c0}" 116 | 117 | for FILE; do 118 | if [ ! -f "$FILE" ] || [ ! -r "$FILE" ]; then 119 | printf "${c031}%s${c0}: Not a readable file: ${c036}%s${c0}\\n" \ 120 | Error "$FILE" 121 | continue 122 | fi 123 | printf "Uploading ${c036}%s${c0} ... " "$FILE" 124 | upload_release_asset "$FILE" \ 125 | && echo "${c032}done${c0}" || echo "${c031}fail${c0}" 126 | done 127 | 128 | printf "Publishing release ${c036}%s${c0} ... " "$TAG" 129 | publish_release "$RELEASE_ID" \ 130 | && echo "${c032}done${c0}" || echo "${c031}fail${c0}" 131 | -------------------------------------------------------------------------------- /bin/hostnames.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Updates hostnames for the given IP or 127.0.0.1 in /etc/hosts. 5 | # 6 | # Usage: ./hostnames.sh [-i IP] [config_file_1] [config_file_2] [...] 7 | # 8 | # The default configuration file is "$PWD/hostnames". 9 | # 10 | # If the provided IP string is empty, the hostname entries are removed. 11 | # 12 | # Each hostname in the configuration files must be separated by a new line. 13 | # Empty lines and lines starting with a hash (#) will be ignored. 14 | # 15 | # Copyright 2015, Sebastian Tschan 16 | # https://blueimp.net 17 | # 18 | # Licensed under the MIT license: 19 | # https://opensource.org/licenses/MIT 20 | # 21 | 22 | set -e 23 | 24 | if [ "$1" = '-i' ]; then 25 | IP=$2 26 | shift 2 27 | else 28 | IP=127.0.0.1 29 | fi 30 | 31 | if [ $# = 0 ]; then 32 | # Use "$PWD/hostnames" as default configuration file: 33 | set -- "$PWD/hostnames" 34 | fi 35 | 36 | # Replaces everything but alphanumeric characters with dashes: 37 | sanitize() { 38 | echo "$1" | sed 's/[^a-zA-Z0-9-]/-/g' 39 | } 40 | 41 | # Returns a marker to identify the hostname settings in /etc/hosts: 42 | marker() { 43 | # Use the config file folder as project name: 44 | project="$(sanitize "$(basename "$(cd "$(dirname "$1")" && pwd)")")" 45 | config_name="$(sanitize "$(basename "$1")")" 46 | echo "## $project $config_name" 47 | } 48 | 49 | # Updates hosts from STDIN with the mappings in the given config file: 50 | map_hostnames() { 51 | marker_base="$(marker "$1")" 52 | marker_start="$marker_base start" 53 | marker_end="$marker_base end" 54 | # Remove the current hostnames section: 55 | sed "/$marker_start/,/$marker_end/d" 56 | # Don't add any entries unless IP is set: 57 | if [ -z "$IP" ]; then return; fi 58 | # Add the new hostname settings: 59 | echo "$marker_start" 60 | while read -r line; do 61 | # Skip empty lines and lines starting with a hash (#): 62 | if [ -z "$line" ] || [ "${line#\#}" != "$line" ]; then continue; fi 63 | # Add each hostname entry with the $IP as mapping: 64 | printf '%s\t%s\n' "$IP" "$line" 65 | done < "$1" 66 | echo "$marker_end" 67 | } 68 | 69 | get_hosts_content() { 70 | # Retrieve the current host settings: 71 | hosts_content="$(cat /etc/hosts)" 72 | for file; do 73 | if [ ! -f "$file" ]; then 74 | echo "$file is not a valid file." >&2 75 | continue 76 | fi 77 | # Update the mappings for each configuration file: 78 | hosts_content="$(echo "$hosts_content" | map_hostnames "$file")" 79 | done 80 | echo "$hosts_content" 81 | } 82 | 83 | # Updates /etc/hosts with the given content after confirmation from the user: 84 | update_hosts() { 85 | hosts_content="$1" 86 | # Diff /etc/hosts with the new content: 87 | if hosts_diff="$(echo "$hosts_content" | diff /etc/hosts -)"; then 88 | echo 'No updates to /etc/hosts required.' 89 | return 90 | fi 91 | # Show a confirmation prompt to the user: 92 | echo 93 | echo "$hosts_diff" 94 | echo 95 | echo 'Update /etc/hosts with the given changes?' 96 | echo 'This will require Administrator privileges.' 97 | echo 'Please type "y" if you wish to proceed.' 98 | read -r confirmation 99 | if [ "$confirmation" = "y" ]; then 100 | # Check if we have root access: 101 | if [ "$(id -u)" -eq 0 ]; then 102 | echo "$hosts_content" > /etc/hosts 103 | else 104 | # Get root access and then write the new hosts file: 105 | echo "$hosts_content" | sudo tee /etc/hosts > /dev/null 106 | fi 107 | # Check if the last command failed: 108 | # shellcheck disable=SC2181 109 | if [ $? -eq 0 ]; then 110 | echo "Successfully updated /etc/hosts." 111 | return 112 | else 113 | echo "Update of /etc/hosts failed." >&2 114 | return 1 115 | fi 116 | fi 117 | echo "No updates to /etc/hosts written." 118 | } 119 | 120 | update_hosts "$(get_hosts_content "$@")" 121 | -------------------------------------------------------------------------------- /bin/parallelize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Executes a given command for each STDIN line in parallel. 5 | # The command is called with the given arguments and the current line. 6 | # The output of each command is printed in a separate section. 7 | # Empty lines and lines starting with a hash (#) are skipped. 8 | # 9 | # Usage: echo "$DATA" | ./parallelize.sh [-q] [-s] [-f format] command [args...] 10 | # 11 | # Quite mode (-q) prints only command output. 12 | # Sequential mode (-s) runs the commands sequentially instead of in parallel. 13 | # A printf format string (-f format) can be defined to format the line before 14 | # passing it on as argument to the command. 15 | # 16 | # Copyright 2016, Sebastian Tschan 17 | # https://blueimp.net 18 | # 19 | # Licensed under the MIT license: 20 | # https://opensource.org/licenses/MIT 21 | # 22 | 23 | set -e 24 | 25 | # Color codes: 26 | c031='\033[0;31m' # red 27 | c032='\033[0;32m' # green 28 | c036='\033[0;36m' # cyan 29 | c0='\033[0m' # no color 30 | 31 | # Prints the given error message and exits: 32 | error_exit() { 33 | echo "${c031}$1${c0}" >&2 34 | echo "Usage: $0 [-e script] [-q] [-s] command [args...]" >&2 35 | exit 1 36 | } 37 | 38 | # Deletes the temp dir including the output files: 39 | cleanup() { 40 | rm -rf "$TMP_DIR" 41 | } 42 | 43 | # Prints output unless in quiet mode: 44 | prints() { 45 | if [ -z "$QUIET_MODE" ]; then echo "$@"; fi 46 | } 47 | 48 | # Formatted output unless in quiet mode: 49 | printfs() { 50 | # shellcheck disable=SC2059 51 | if [ -z "$QUIET_MODE" ]; then printf "$@"; fi 52 | } 53 | 54 | # Returns the length of the longest line in this parallel execution: 55 | max_line_length() { 56 | cat "$TMP_DIR/max_line_length" 57 | } 58 | 59 | # Execute the given command in parallel: 60 | execute_parallel() { 61 | # Filter out empty lines and lines starting with a hash (#): 62 | if [ -z "$LINE" ] || [ "${LINE#\#}" != "$LINE" ]; then return; fi 63 | INDEX=$((INDEX+1)) 64 | LINE_LENGTH=${#LINE} 65 | # Store the LINE in a file: 66 | LINE_PATH="$TMP_DIR/$(printf '%03d' "$INDEX")" 67 | echo "$LINE" > "$LINE_PATH" 68 | if [ "$LINE_LENGTH" -gt "$MAX_LINE_LENGTH" ]; then 69 | MAX_LINE_LENGTH=$LINE_LENGTH 70 | echo "$MAX_LINE_LENGTH" > "$TMP_DIR/max_line_length" 71 | fi 72 | # Run each command in parallel and store the output in a temp file: 73 | # shellcheck disable=SC2059 74 | if "$@" "$(printf "${FORMAT:-%s}" "$LINE")" > "$LINE_PATH".out 2>&1; then 75 | printfs "${c036}%-$(max_line_length)s${c0} ${c032}done${c0}\n" "$LINE" 76 | else 77 | printfs "${c036}%-$(max_line_length)s${c0} ${c031}failed${c0}\n" "$LINE" 78 | fi & 79 | } 80 | 81 | # Executes the given command sequentially: 82 | execute_sequential() { 83 | # Filter out empty lines and lines starting with a hash (#): 84 | if [ -z "$LINE" ] || [ "${LINE#\#}" != "$LINE" ]; then return; fi 85 | prints 86 | prints "${c036}$LINE${c0}" 87 | prints '====================' 88 | # Run each command sequentially: 89 | # shellcheck disable=SC2059 90 | "$@" "$(printf "${FORMAT:-%s}" "$LINE")" 91 | prints '====================' 92 | } 93 | 94 | # Iterate over the lines from STDIN and executes the given command for each: 95 | read_lines() { 96 | while read -r LINE; do 97 | "$@" 98 | done 99 | } 100 | 101 | QUIET_MODE= 102 | SEQUENTIAL_MODE= 103 | FORMAT= 104 | INDEX=0 105 | MAX_LINE_LENGTH=0 106 | 107 | # Parse command-line options: 108 | while getopts ':qsf:' opt; do 109 | case "$opt" in 110 | q) QUIET_MODE=true;; 111 | s) SEQUENTIAL_MODE='true';; 112 | f) FORMAT=$OPTARG;; 113 | \?) error_exit "Invalid option: -$OPTARG";; 114 | :) error_exit "Option -$OPTARG requires an argument.";; 115 | esac 116 | done 117 | 118 | # Remove the parsed options from the command-line arguments: 119 | shift $((OPTIND-1)) 120 | 121 | if [ $# = 0 ]; then 122 | error_exit 'Missing command argument.' 123 | fi 124 | 125 | if [ -n "$SEQUENTIAL_MODE" ]; then 126 | read_lines execute_sequential "$@" 127 | prints 128 | exit 129 | fi 130 | 131 | # Create a temp dir for output files: 132 | TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}"/"$(basename "$0")"-XXXXXXXXXX) 133 | 134 | # Clean up after terminating child processes on SIGINT and SIGTERM: 135 | trap 'wait;cleanup' INT TERM 136 | 137 | prints 138 | prints 'Please wait ...' 139 | 140 | read_lines execute_parallel "$@" 141 | 142 | # Wait for all child processes to terminate: 143 | wait 144 | 145 | # Print the content of the output files 146 | for FILE in "$TMP_DIR"/*.out; do 147 | # If no files are found, the unexpanded pattern is returned as result: 148 | [ "$FILE" = "$TMP_DIR/*.out" ] && break 149 | prints 150 | # The line is stored in a file without ".out" extension: 151 | prints "# ${c036}$(cat "${FILE%\.out}")${c0}" 152 | prints '====================' 153 | cat "$FILE" 154 | prints '====================' 155 | done 156 | 157 | prints 158 | 159 | cleanup 160 | -------------------------------------------------------------------------------- /bin/docker-build-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Builds images for each Dockerfile found recursively in the given directory. 5 | # Resolves image dependencies for images in the same organization. 6 | # Tags images based on the directory structure and git branch names. 7 | # 8 | # Usage: ./docker-build-images.sh [Dockerfile|directory] [...] 9 | # 10 | # The parent directory basename is used as the user/organization name. 11 | # The current directory basename is used as the repository name. 12 | # The branch is used as the version, with "master" being tagged as "latest". 13 | # e.g.: parentdir/currentdir:latest 14 | # 15 | # If DOCKER_ORG is defined, it is used as the user/organization name. 16 | # If DOCKER_HUB is defined, it is prefixed to the user/organization name. 17 | # e.g.: $DOCKER_HUB/$DOCKER_ORG/repository:latest 18 | # 19 | # Copyright 2015, Sebastian Tschan 20 | # https://blueimp.net 21 | # 22 | # Licensed under the MIT license: 23 | # https://opensource.org/licenses/MIT 24 | # 25 | 26 | # Normalizes according to docker hub organization/image naming conventions: 27 | normalize() { 28 | echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]//g' 29 | } 30 | 31 | # Build and tag the image based on the git branches in the current directory: 32 | build_versions() { 33 | image="$1" 34 | shift 35 | if [ ! -d '.git' ]; then 36 | # Not a git repository, so simply build a "latest" image version: 37 | docker build -t "$image" "$@" . 38 | return $? 39 | fi 40 | current_branch=$(git rev-parse --abbrev-ref HEAD) 41 | # Iterate over all branches: 42 | branches=$(git for-each-ref --format='%(refname:short)' refs/heads/) 43 | for branch in $branches; do 44 | git checkout "$branch" 45 | # Tag master as "latest": 46 | if [ "$branch" = 'master' ]; then 47 | branch='latest' 48 | fi 49 | # Normalize the branch name: 50 | branch="$(normalize "$branch")" 51 | # Build and tag the image with the branch name: 52 | docker build -t "$image:$branch" "$@" . 53 | done 54 | git checkout "$current_branch" 55 | } 56 | 57 | # Builds an image for each git branch of the given Dockerfile directory: 58 | build() { 59 | cwd="$PWD" 60 | file="$(basename "$1")" 61 | dir="$(dirname "$1")" 62 | cd "$dir" || return 1 63 | organization="$DOCKER_ORG" 64 | if [ -z "$organization" ]; then 65 | # Use the parent folder for the organization/user name: 66 | organization="$(cd .. && normalize "$(basename "$PWD")")" 67 | fi 68 | if [ -n "$DOCKER_HUB" ]; then 69 | organization="$DOCKER_HUB/$organization" 70 | fi 71 | # Use the current folder for the image name: 72 | image="$organization/$(normalize "$(basename "$PWD")")" 73 | # Check if the image depends on another image of the same organization: 74 | from=$(grep "^FROM $organization/" "$file" | awk '{print $2}') 75 | # If it does, only build if the image is already available: 76 | if [ -z "$from" ] || docker inspect "$from" > /dev/null 2>&1; then 77 | build_versions "$image" -f "$file" 78 | else 79 | echo "$image requires $from ..." >&2 && false 80 | fi 81 | status=$? 82 | cd "$cwd" || return 1 83 | return $status 84 | } 85 | 86 | # Builds and tags images for each Dockerfile in the arguments list: 87 | build_images() { 88 | # Set the maximum number of calls on the first run: 89 | if [ "$MAX_CALLS" = 0 ]; then 90 | # Worst case scenario needs n*(n+1)/2 calls for dependency resolution, 91 | # which is the sum of all natural numbers (1+2+3+4+...n). 92 | # n is the number of arguments (=Dockerfiles) provided: 93 | MAX_CALLS=$(($#*($#+1)/2)) 94 | fi 95 | CALLS=$((CALLS+1)) 96 | if [ $CALLS -gt $MAX_CALLS ]; then 97 | echo 'Could not resolve image dependencies.' >&2 98 | return 1 99 | fi 100 | for file; do 101 | # Shift the arguments list to remove the current Dockerfile: 102 | shift 103 | # Basic check if the file is a valid Dockerfile: 104 | if ! grep '^FROM ' "$file"; then 105 | echo "Invalid Dockerfile: $file" >&2 106 | continue 107 | fi 108 | if ! build "$file"; then 109 | # The current build requires another image as dependency, 110 | # so we add it to the end of the build list and start over: 111 | build_images "$@" "$file" 112 | return $? 113 | fi 114 | done 115 | } 116 | 117 | MAX_CALLS=0 118 | CALLS=0 119 | NEWLINE=' 120 | ' 121 | 122 | # Parses the arguments, finds Dockerfiles and starts the builds: 123 | init() { 124 | args= 125 | for arg; do 126 | if [ -d "$arg" ]; then 127 | # Search for Dockerfiles and add them to the list: 128 | args="$args$NEWLINE$(find "$arg" -name Dockerfile)" 129 | else 130 | args="$args$NEWLINE$arg" 131 | fi 132 | done 133 | # Set the list as arguments, splitting only at newlines: 134 | IFS="$NEWLINE"; 135 | # shellcheck disable=SC2086 136 | set -- $args; 137 | unset IFS 138 | build_images "$@" 139 | } 140 | 141 | init "${@:-.}" 142 | -------------------------------------------------------------------------------- /bin/envconfig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Wrapper script to write environment variables in config files. 5 | # Replaces placeholders and creates files, then starts the given command. 6 | # Supports multiline variables, reading from file paths and base64 encoded data. 7 | # 8 | # Usage: ./envconfig.sh [-f config_file] [command] [args...] 9 | # 10 | # The default envconfig configuration file is "/usr/local/etc/envconfig.conf". 11 | # An alternate configuration file can be provided via -f option. 12 | # To read the configuration from STDIN, the placeholder "-" can be used. 13 | # 14 | # Each line of the configuration for envconfig must have the following format: 15 | # VARIABLE_NAME /absolute/path/to/config/file 16 | # 17 | # Each mapped variable will be unset before the command given to envconfig is 18 | # run, unless the variable name is prefixed with an exclamation mark: 19 | # !VARIABLE_NAME /absolute/path/to/config/file 20 | # 21 | # Empty lines and lines starting with a hash (#) will be ignored. 22 | # Multiple mappings of the same VARIABLE_NAME or path are possible. 23 | # 24 | # Placeholders in config files must have the following format: 25 | # {{VARIABLE_NAME}} 26 | # 27 | # Variable content can be provided from a file location, given the following: 28 | # The file path must be provided in a variable with "_FILE" suffix. 29 | # The file contents will then be used for the variable without the prefix. 30 | # For example, the contents of a file at $DATA_FILE will be used as $DATA. 31 | # 32 | # Variable content can be provided in base64 encoded form, given the following: 33 | # The base64 data must be provided in a variable with "B64_" prefix. 34 | # The decoded data will then be used for the variable without the prefix. 35 | # For example, the content of $B64_DATA will be decoded and used as $DATA. 36 | # 37 | # Copyright 2015, Sebastian Tschan 38 | # https://blueimp.net 39 | # 40 | # Licensed under the MIT license: 41 | # https://opensource.org/licenses/MIT 42 | # 43 | 44 | set -e 45 | 46 | # Returns the platform dependent base64 decode argument: 47 | b64_decode_arg() { 48 | if [ "$(echo 'eA==' | base64 -d 2> /dev/null)" = 'x' ]; then 49 | printf %s -d 50 | else 51 | printf %s --decode 52 | fi 53 | } 54 | 55 | # Interpolates the given variable name: 56 | interpolate() { 57 | if [ "$1" = '_' ] || ! expr "$1" : '[a-zA-Z_][a-zA-Z0-9_]*' 1>/dev/null; then 58 | echo "Invalid variable name: $1" >&2 && return 1 59 | fi 60 | # Check if a variable with the given name plus "_FILE" suffix exists: 61 | if eval 'test ! -z "${'"$1"'_FILE+x}"'; then 62 | # Read the contents from the interpolated file path: 63 | eval 'cat "${'"$1"'_FILE}"' 64 | # Check if a variable with the given name plus "B64_" prefix exists: 65 | elif eval 'test ! -z "${B64_'"$1"'+x}"'; then 66 | # Return the decoded content of the "B64_" prefixed variable: 67 | eval 'echo "$B64_'"$1"'"' | tr -d '\n' | base64 "$B64_DECODE_ARG" 68 | else 69 | # Interpolate the name as environment variable, print to stderr if unset: 70 | eval 'printf "%s" "${'"$1"'?}"' 71 | fi 72 | } 73 | 74 | # Global search and replace with the given pattern and replacement arguments: 75 | gsub() { 76 | # In sed replacement strings, slash, backslash and ampersand must be escaped. 77 | # Multiline strings are allowed, but must escape newlines with a backslash. 78 | # Therefore, the last sed sub call adds a backslash to all but the last line: 79 | sed "s/$1/$(echo "$2" | sed 's/[/\&]/\\&/g;$!s/$/\\/g')/g" 80 | } 81 | 82 | # Parses the given config file and writes the env config: 83 | write_envconfig() { 84 | # Store variables to unset in a space-separated list: 85 | unset_variables= 86 | # Set the platform dependent base64 decode argument: 87 | B64_DECODE_ARG="$(b64_decode_arg)" 88 | # Iterate over each line of the config file: 89 | while read -r line; do 90 | # Skip empty lines and lines starting with a hash (#): 91 | [ -z "$line" ] || [ "${line#\#}" != "$line" ] && continue 92 | # Extract the substring up to the first space as variable name: 93 | name="${line%% *}" 94 | # Check if the variable should be unset (no exclamation mark prefix): 95 | if [ "${name#!}" = "$name" ]; then 96 | # Store the name and its variants in the list of variables to unset: 97 | unset_variables="$unset_variables $name ${name}_FILE B64_$name" 98 | else 99 | # Remove the exclamation mark prefix: 100 | name="${name#!}" 101 | fi 102 | # Extract the substring after the last space as file path: 103 | path="${line##* }" 104 | # Check if the file exists and has a size greater than zero: 105 | if [ -s "$path" ]; then 106 | tmpfile="$(mktemp "${TMPDIR:-/tmp}/$name.XXXXXXXXXX")" 107 | # Replace the placeholder with the environment variable: 108 | gsub "{{$name}}" "$(interpolate "$name")" < "$path" > "$tmpfile" 109 | # Override the original file without changing permissions or ownership: 110 | cat "$tmpfile" > "$path" && rm "$tmpfile" 111 | else 112 | # Create the path if it doesn't exist: 113 | mkdir -p "$(dirname "$path")" 114 | # Set the environment variable as file content: 115 | interpolate "$name" >> "$path" 116 | fi 117 | # Use the given config file as input: 118 | done < "$1" 119 | # Unset the given variables: 120 | # shellcheck disable=SC2086 121 | unset $unset_variables 122 | } 123 | 124 | # Write the environment config using the provided configuration file: 125 | if [ "$1" = "-f" ]; then 126 | if [ "$2" = - ]; then 127 | write_envconfig /dev/stdin 128 | else 129 | write_envconfig "$2" 130 | fi 131 | shift 2 132 | else 133 | # Use the default config file to write the env config: 134 | write_envconfig '/usr/local/etc/envconfig.conf' 135 | fi 136 | 137 | # Execute the given command (with the given arguments): 138 | exec "$@" 139 | -------------------------------------------------------------------------------- /bin/mail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Sends email to the given SMTP server via Netcat/OpenSSL. 5 | # Supports TLS, STARTTLS and AUTH LOGIN. 6 | # 7 | # Usage: 8 | # echo 'Text' | ./mail.sh [-h host] [-p port] [-f from] [-t to] [-s subject] \ 9 | # [-c user[:pass]] [-e tls|starttls] 10 | # 11 | # Copyright 2016, Sebastian Tschan 12 | # https://blueimp.net 13 | # 14 | # Licensed under the MIT license: 15 | # https://opensource.org/licenses/MIT 16 | # 17 | 18 | set -e 19 | 20 | # Default settings: 21 | HOST=localhost 22 | PORT=25 23 | USER=${USER:-user} 24 | HOSTNAME=${HOSTNAME:-localhost} 25 | FROM="$USER <$USER@$HOSTNAME>" 26 | TO='test ' 27 | SUBJECT=Test 28 | 29 | NEWLINE=' 30 | ' 31 | 32 | print_usage() { 33 | echo "Usage: echo 'Text' | $0" \ 34 | '[-h host] [-p port] [-f from] [-t to] [-s subject]' \ 35 | '[-c user[:pass]] [-e tls|starttls]' 36 | } 37 | 38 | # Prints the given error and optionally a usage message and exits: 39 | error_exit() { 40 | echo "Error: $1" >&2 41 | if [ -n "$2" ]; then 42 | print_usage >&2 43 | fi 44 | exit 1 45 | } 46 | 47 | # Adds brackets around the last word in the given address, trims whitespace: 48 | normalize_address() { 49 | address=$(echo "$1" | awk '{$1=$1};1') 50 | if [ "${address%>}" = "$address" ]; then 51 | echo "$address" | sed 's/[^ ]*$/<&>/' 52 | else 53 | echo "$address" 54 | fi 55 | } 56 | 57 | # Does a simple validity check on the email address format, 58 | # without support for comments or for quoting in the local-part: 59 | validate_email() { 60 | local_part=${1%%@*>} 61 | local_part=$(echo "${local_part#<}" | sed 's/[][[:cntrl:][:space:]"(),:;\]//') 62 | domain=${1##<*@} 63 | domain=$(echo "${domain%>}" | LC_CTYPE=UTF-8 sed 's/[^][[:alnum:].:-]//') 64 | if [ "<$local_part@$domain>" != "$1" ]; then 65 | error_exit "Invalid email address: $1" 66 | fi 67 | } 68 | 69 | is_printable_ascii() { 70 | (LC_CTYPE=C; case "$1" in *[![:print:]]*) return 1;; esac) 71 | } 72 | 73 | # Encodes the given string according to RFC 1522: 74 | # https://tools.ietf.org/html/rfc1522 75 | rfc1342_encode() { 76 | if is_printable_ascii "$1"; then 77 | printf %s "$1" 78 | else 79 | printf '=?utf-8?B?%s?=' "$(printf %s "$1" | base64)" 80 | fi 81 | } 82 | 83 | encode_address() { 84 | email="<${1##*<}" 85 | if [ "$email" != "$1" ]; then 86 | name="${1%<*}" 87 | # Remove any trailing space as we add it again in the next line: 88 | name="${name% }" 89 | echo "$(rfc1342_encode "$name") $email" 90 | else 91 | echo "$1" 92 | fi 93 | } 94 | 95 | parse_recipients() { 96 | addresses=$(echo "$TO" | tr ',' '\n') 97 | IFS="$NEWLINE" 98 | for address in $addresses; do 99 | address=$(normalize_address "$address") 100 | email="<${address##*<}" 101 | validate_email "$email" 102 | output="$output, $(encode_address "$address")" 103 | recipients="$recipients$NEWLINE$email" 104 | done 105 | unset IFS 106 | # Remove the first commma and space from the address list: 107 | TO="$(echo "$output" | cut -c 3-)" 108 | # Remove leading blank line from the recipients list and add header prefixes: 109 | RECIPIENTS_HEADERS="$(echo "$recipients" | sed '/./,$!d; s/^/RCPT TO:/')" 110 | } 111 | 112 | parse_sender() { 113 | FROM="$(normalize_address "$FROM")" 114 | email="<${FROM##*<}" 115 | validate_email "$email" 116 | FROM="$(encode_address "$FROM")" 117 | SENDER_HEADER="MAIL FROM:$email" 118 | } 119 | 120 | parse_text() { 121 | CONTENT_TRANSFER_ENCODING=7bit 122 | TEXT= 123 | while read -r line; do 124 | # Use base64 encoding if the text contains non-printable ASCII characters 125 | # or exceeds 998 characters (excluding the \r\n line endings): 126 | if ! is_printable_ascii "$line" || [ "${#line}" -gt 998 ]; then 127 | CONTENT_TRANSFER_ENCODING=base64 128 | fi 129 | TEXT="$TEXT$line$NEWLINE" 130 | done 131 | if [ "$CONTENT_TRANSFER_ENCODING" = base64 ]; then 132 | TEXT="$(printf %s "$TEXT" | base64)" 133 | else 134 | # Prepend each period at the start of a line with another period, 135 | # to follow RFC 5321 Section 4.5.2 Transparency guidelines: 136 | TEXT="$(printf %s "$TEXT" | sed 's/^\./.&/g')" 137 | fi 138 | } 139 | 140 | parse_subject() { 141 | SUBJECT="$(rfc1342_encode "$SUBJECT")" 142 | } 143 | 144 | set_date() { 145 | DATE=$(date '+%a, %d %b %Y %H:%M:%S %z') 146 | } 147 | 148 | parse_credentials() { 149 | USERNAME=${CREDENTIALS%%:*} 150 | if [ -z "${CREDENTIALS##*:*}" ]; then 151 | PASSWORD=${CREDENTIALS#*:}; 152 | fi 153 | if [ -n "$USERNAME" ]; then 154 | GREETING="EHLO $HOSTNAME" 155 | GREETING="$GREETING${NEWLINE}AUTH LOGIN" 156 | GREETING="$GREETING${NEWLINE}$(printf %s "$USERNAME" | base64)" 157 | GREETING="$GREETING${NEWLINE}$(printf %s "$PASSWORD" | base64)" 158 | else 159 | GREETING="HELO $HOSTNAME" 160 | fi 161 | } 162 | 163 | replace_newlines() { 164 | awk '{printf "%s\r\n", $0}' 165 | } 166 | 167 | send_mail() { 168 | case "$ENCRYPTION" in 169 | starttls) openssl s_client -starttls smtp -quiet -connect "$HOST:$PORT";; 170 | tls) openssl s_client -quiet -connect "$HOST:$PORT";; 171 | '') nc "$HOST" "$PORT";; 172 | *) error_exit "Invalid encryption mode: $ENCRYPTION" true;; 173 | esac 174 | } 175 | 176 | while getopts ':h:p:f:t:s:c:e:' OPT; do 177 | case "$OPT" in 178 | h) HOST=$OPTARG;; 179 | p) PORT=$OPTARG;; 180 | f) FROM=$OPTARG;; 181 | t) TO=$OPTARG;; 182 | s) SUBJECT=$OPTARG;; 183 | c) CREDENTIALS=$OPTARG;; 184 | e) ENCRYPTION=$OPTARG;; 185 | :) error_exit "Option -$OPTARG requires an argument." true;; 186 | \?) error_exit "Invalid option: -$OPTARG" true;; 187 | esac 188 | done 189 | 190 | set_date 191 | parse_recipients 192 | parse_sender 193 | parse_text 194 | parse_subject 195 | parse_credentials 196 | 197 | MAIL="$GREETING"' 198 | '"$SENDER_HEADER"' 199 | '"$RECIPIENTS_HEADERS"' 200 | DATA 201 | Content-Type: text/plain; charset=utf-8 202 | Content-Transfer-Encoding: '"$CONTENT_TRANSFER_ENCODING"' 203 | Date: '"$DATE"' 204 | From: '"$FROM"' 205 | To: '"$TO"' 206 | Subject: '"$SUBJECT"' 207 | 208 | '"$TEXT"' 209 | . 210 | QUIT' 211 | 212 | echo "$MAIL" | replace_newlines | send_mail 213 | -------------------------------------------------------------------------------- /bin/pull-repository.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Pulls or clones the repository given as url or directory. 5 | # Fast-forward merges remote origin branches. 6 | # Allows to run a command for each updated branch. 7 | # 8 | # Usage: ./pull-repository.sh [-b branches] [url|dir] [-- comand [args...]] 9 | # 10 | # The "-b" option defines a whitespace-separated list of branches to merge. 11 | # 12 | # Copyright 2016, Sebastian Tschan 13 | # https://blueimp.net 14 | # 15 | # Licensed under the MIT license: 16 | # https://opensource.org/licenses/MIT 17 | # 18 | 19 | # Exit immediately if a command exits with a non-zero status: 20 | set -e 21 | 22 | # Color codes: 23 | c031='\033[0;31m' # red 24 | c032='\033[0;32m' # green 25 | c033='\033[0;33m' # yellow 26 | c036='\033[0;36m' # cyan 27 | c0='\033[0m' # no color 28 | 29 | # Prints the given string in a highlight color: 30 | highlight() { 31 | echo "${c036}$1${c0}" 32 | } 33 | 34 | # Prints the given string in a success color: 35 | success() { 36 | echo "${c032}$1${c0}" 37 | } 38 | 39 | # Prints the given string in a warning color: 40 | warning() { 41 | echo "${c033}$1${c0}" 42 | } 43 | 44 | # Prints the given string in an error color: 45 | error() { 46 | echo "${c031}$1${c0}" 47 | } 48 | 49 | # Prints the given error message and exits: 50 | error_exit() { 51 | error "$1" >&2 52 | echo "Usage: $0 [-b branches] [-c command] url" >&2 53 | exit 1 54 | } 55 | 56 | # Prints a list of all local branches that track an upstream branch: 57 | local_upstream_branches() { 58 | git for-each-ref --format='%(refname:short) %(upstream)' refs/heads/ | 59 | grep -w refs/remotes | grep -Eo '^[^ ]+' 60 | } 61 | 62 | # Checks if the given branch exists: 63 | branch_exists() { 64 | if ! git show-ref --verify -q "refs/remotes/origin/$1"; then 65 | echo "$(highlight "$1") $(warning 'not found')" >&2 66 | return 1 67 | fi 68 | } 69 | 70 | # Attempts a fast-forward merge for the given branch: 71 | fast_forward_merge() { 72 | branch_exists "$1" || return $? 73 | status=0 74 | tracking_status=$(git for-each-ref --format='%(push:trackshort)' \ 75 | "refs/heads/$1") 76 | if [ "$tracking_status" = '<>' ]; then 77 | echo "$(highlight "$1") $(error 'has diverged')" >&2 78 | return 1 79 | elif [ "$tracking_status" = '=' ]; then 80 | echo "$(highlight "$1") $(success 'is up-to-date')" 81 | return 82 | elif [ "$tracking_status" = '>' ]; then 83 | echo "$(highlight "$1") $(success 'is ahead')" 84 | return 85 | elif [ "$tracking_status" = '<' ]; then 86 | # Create a local copy of the remote branch: 87 | git branch --quiet "origin/$1" "origin/$1" 88 | # Put the working copy into a detached-head state, to allow branch merges: 89 | git checkout --detach --quiet 90 | # Attempt a fast-forward merge with the local copy of the remote branch: 91 | git fetch --quiet . "origin/$1:$1" || status=$? 92 | # Check out the previous working copy: 93 | git checkout --quiet - 94 | # Delete the local copy of the remote branch: 95 | git branch -D --quiet "origin/$1" 96 | else 97 | # The remote branch has not been fetched yet: 98 | git fetch --quiet origin "$1:$1" || status=$? 99 | fi 100 | if [ $status -eq 0 ]; then 101 | echo "$(highlight "$1") $(success 'has been updated')" 102 | else 103 | echo "$(highlight "$1") $(error 'update failed')" >&2 104 | fi 105 | return $status 106 | } 107 | 108 | # Checks if fast-forward merge is enabled: 109 | fast_forward_merge_enabled() { 110 | if [ "$(git config merge.ff)" = 'no' ]; then 111 | error 'git fast-forward merge disabled' >&2 112 | global_arg= 113 | [ "$(git config --global merge.ff)" = 'no' ] && global_arg=' --global' 114 | echo 'Run the following command to enable it:' >&2 115 | highlight "git config$global_arg merge.ff yes" >&2 116 | exit 1 117 | fi 118 | } 119 | 120 | # Returns the selected branches: 121 | get_branches() { 122 | if [ -z "$BRANCHES" ]; then 123 | BRANCHES=$(local_upstream_branches) 124 | fi 125 | printf %s "$BRANCHES" 126 | } 127 | 128 | # Pulls the selected branches of the given repository directory: 129 | pull() { 130 | cd "$1" 131 | fast_forward_merge_enabled || return $? 132 | git fetch --quiet origin 133 | pull_status=0 134 | for branch in $(get_branches); do 135 | fast_forward_merge "$branch" || pull_status=$? 136 | done 137 | return $pull_status 138 | } 139 | 140 | # Clones the given repository url: 141 | clone() { 142 | if git clone --quiet "$1" "$2"; then 143 | echo "$(highlight "$2") $(success 'cloned')" 144 | cd "$2" 145 | else 146 | echo "$(highlight "$1") $(error 'clone failed')" >&2 147 | return 1 148 | fi 149 | } 150 | 151 | # Clones or pulls the given repository: 152 | update_repository() { 153 | if [ -d "$1" ]; then 154 | pull "$1" 155 | else 156 | repo_base=$(basename "$1") 157 | dir=${repo_base%.git} 158 | if [ -d "$dir" ]; then 159 | pull "$dir" 160 | else 161 | clone "$1" "$dir" 162 | fi 163 | fi 164 | return $? 165 | } 166 | 167 | # Checks out the given branch, throwing away local changes: 168 | clean_checkout() { 169 | git checkout --force --quiet "$1" 170 | } 171 | 172 | # Runs the COMMAND on the given branch: 173 | execute_on_branch() { 174 | branch_exists "$1" || return $? 175 | branch=$1 176 | shift 177 | stashed=false 178 | if ! git diff --quiet || ! git diff --cached --quiet; then 179 | # Stash the current working directory changes: 180 | git stash --quiet 181 | stashed=true 182 | fi 183 | # Remember the current branch: 184 | current_branch=$(git rev-parse --abbrev-ref HEAD) 185 | # Checkout the given one: 186 | [ "$current_branch" != "$branch" ] && clean_checkout "$branch" 187 | status=0 188 | # Execute the given command: 189 | eval "$@" || status=$? 190 | # Checkout the original branch: 191 | [ "$current_branch" != "$branch" ] && clean_checkout "$current_branch" 192 | if [ "$stashed" = true ]; then 193 | # Re-apply the stashed changes: 194 | git stash pop --quiet 195 | fi 196 | return $status 197 | } 198 | 199 | # Execute the given command on each selected branch: 200 | execute() { 201 | [ "$#" -eq 0 ] && return 202 | execute_status=0 203 | for branch in $(get_branches); do 204 | execute_on_branch "$branch" "$@" || execute_status=$? 205 | done 206 | return $execute_status 207 | } 208 | 209 | if [ "$1" = -b ]; then 210 | BRANCHES=$2 211 | shift 2 212 | fi 213 | 214 | if [ "$2" = -- ]; then 215 | REPO=$1 216 | shift 2 217 | elif [ "$1" = -- ]; then 218 | REPO=. 219 | shift 220 | elif [ -n "$1" ]; then 221 | REPO=$1 222 | shift 223 | else 224 | REPO=. 225 | fi 226 | 227 | update_repository "$REPO" 228 | execute "$@" 229 | --------------------------------------------------------------------------------