Skip to main content
4 of 4
deleted 2 characters in body
Jamal
  • 34.9k
  • 13
  • 133
  • 237

Bash script to clone all public repos or gists for a specified user, optionally to a directory of choice

I wrote this script as a way to backup and/or download quickly a set of repos or gists from a user. I don't have any major concerns but I'm a noob at bash scripting, and I've been scouring the internet putting these pieces together. I would love for someone to take a look and let me know best practices or problems there may be.

#!/usr/bin/env bash

# Check if git is installed, if not bail
if [[ ! "$(type -P 'git')" ]]; then
    printf "$(tput setaf 1)⊘ Error:$(tput sgr0) %s. Aborting!\n" "Git is required to use $(basename "$0")"
    printf "\n"
    printf "Download it at http://git-scm.com"
    exit 2
fi

# Check if jq is installed, if not bail
if [[ ! "$(type -P 'jq')" ]]; then
    printf "$(tput setaf 1)⊘ Error:$(tput sgr0) %s. Aborting!\n" "jq is required to parse JSON." 
    printf "\n"
    printf "Download it at http://stedolan.github.io/jq"
    exit 2
fi

# variables
feed="repos"
path="${HOME}/Downloads"
usage="$(basename "$0"): usage: $(basename "$0") [-h|--help] [-v|--version] [-f|--feed <value>] <github_username> [<path>]"

# Test for known flags
for opt in "$@"
do
    case "$opt" in
        -f | --feed) # choose feed type
            if [[ "$2" == "repos" || "$2" == "gists" ]]; then
                feed="$2"
            else
                printf "%s\n" "-bash: $(basename "$0"): $1: invalid feed type [repos|gists]"
                printf "%s" "$usage"
                exit 1
            fi
            shift 2
            ;;
        -h | --help) # Help text
            printf "\n"
            printf "%s\n" "Options:"
            printf "\n"
            printf "\t%s\n" "-h, --help              Print this help text"
            printf "\t%s\n" "-f, --feed [<value>]    <value> can be either gists or repos, default is repos"
            printf "\t%s\n" "-v, --version           Print out the version"
            printf "\n"
            printf "%s\n" "Documentation can be found at https://github.com/chriopedia/clone-all"
            exit 0
            ;;
        --test) # test suite using roundup
            roundup="$(type -P 'roundup')"
            [[ ! -z $roundup ]] || {
                printf "$(tput setaf 1)⊘ Error:$(tput sgr0) %s. Aborting!\n" "Roundup is required to run tests"
                printf "\n"
                printf "Download it at https://github.com/bmizerany/roundup"
                exit 2;
            }
            $roundup ./tests/*.sh
            exit 0
            ;;
        -v | --version) # Version of software
            printf "%s\n" "Version $(git describe --tags)"
            exit 0
            ;;
        --) # End of all options
            printf "%s\n" "-bash: $(basename "$0"): $1: invalid option"
            printf "%s" "$usage"
            exit 1
            ;;
        -*)
            printf "%s\n" "-bash: $(basename "$0"): $1: invalid option"
            printf "%s" "$usage"
            exit 1
            ;;
        *)  # No more options
            break
            ;;
    esac
done

# Check if username is passed in, if not bail
if [[ -z "$1" ]]; then
    printf "$(tput setaf 1)⊘ Error:$(tput sgr0) %s. Aborting!\n" "A valid Github user is required" 
    exit 3
fi

# check if directory is not blank and exists
if [[ ! -z "$2" && -d "$2" ]]; then
    # http://www.linuxforums.org/forum/programming-scripting/solved-delete-trailing-slashes-using-bash-board-means-print-172714.html
    # This matches from the start of the source string, any 
    # string ending with a non-slash.
    pattern="^.*[^/]"
    # Apply regex
    [[ ${2} =~ $pattern ]]
    # Print the portion of the source string which matched the regex.
    path="${BASH_REMATCH[0]}"
fi

# set some variables
user="$1"
api_url="https://api.github.com/users/${user}/${feed}"
current_page=1
per_page=100

printf "%s" "Checking status of user '${user}'"
# http://stackoverflow.com/questions/238073/how-to-add-a-progress-bar-to-a-shell-script
# start progress bar
while :;do echo -n .;sleep 1;done &

# check response header from github user passed in
# http://stackoverflow.com/a/10724976/1536779
response="$(curl --write-out %{http_code} --silent --output /dev/null "${api_url}")"

# kill the progress bar
kill $!; trap 'kill $!' SIGTERM

# if reponse is greater than or equal to 400 somethings wrong
if [[ "${response}" -ge 400 ]]; then
    printf "%s\n" "-bash: $(basename "$0"): $1: user doesn't exist"
    #debug statement
    printf "%s\n" "Github HTTP Response code: ${response}"
    exit 3
fi


# grab the total number of pages there are
# https://gist.github.com/michfield/4525251
total_pages="$(curl -sI "${api_url}?page=1&per_page=${per_page}" | sed -nE "s/^Link:.*page=([0-9]+)&per_page=${per_page}>; rel=\"last\".*/\1/p")"
if [[ -z ${total_pages} ]]; then
    total_pages=1
fi

# grab a list of repos or gists
# @params $1: page number
# example: get_repos_list 1
get_repos_list() {
    # get a json list of all repos and story as array
    if [[ ${feed} != 'gists' ]]; then
        repos=$(curl -fsSL "${api_url}?page=${1}&per_page=${per_page}" | jq '.[] | .name')
    else
        repos=$(curl -fsSL "${api_url}?page=${1}&per_page=${per_page}" | jq '.[] | .id')
    fi
    echo "$repos"
}

# loop through list of repos at the current page
clone_shit() {
    printf "%s" "Grabbing list of ${feed} for ${user}"
    # http://stackoverflow.com/questions/238073/how-to-add-a-progress-bar-to-a-shell-script
    # start progress bar
    while :;do echo -n .;sleep 1;done &

    # get the list of repos for user
    repos_list=($(get_repos_list "${current_page}"))

    # kill the progress bar
    kill $!; trap 'kill $!' SIGTERM
    printf "\n"

    # loop through all repos in array
    for index in ${!repos_list[*]}
    do
        # variable assignment
        repo="${repos_list[$index]}"
        # Strip quotes from string
        repo="${repo:1:${#repo}-2}"

        if [[ ${feed} != "gists" ]]; then
            printf "%s\n" "Cloning https://github.com/${user}/${repo} to ${path}/repos/${repo}"
            #git clone "https://github.com/${user}/${repo}" "${path}/repos/${repo}"
        else
            printf "%s\n" "Cloning https://gist.github.com/${repo}.git to ${path}/gists/${repo}"
            #git clone "https://gist.github.com/${repo}.git" "${path}/gists/${repo}"
        fi
    done
}

printf "\n"

clone_shit
if [[ ${total_pages} -gt 1 && ${current_page} -lt ${total_pages} ]]; then
    current_page=$((current_page + 1))
    clone_shit
fi
iamnewton
  • 225
  • 1
  • 5