Skip to main content

Bash script for cleaning Visual Studio Project build directories (e.g.: bin/, obj/, etc.) from a particular path, recursively.

#!/usr/bin/env bash

#
# Delete dotnet build dirs and files (e.g. 'bin', 'obj', etc...)
# recursively, starting from the current working directory (PWD).
#
# TODO:
# - Handle unknown options passed in as arguments
#
# Author: Jon LaBelle
# Snippet: https://jonlabelle.com/snippets/view/shell/clean-visual-studio-project-build-files-sh
# Gist: https://gist.github.com/jonlabelle/41e67b8b1eac69aab0e5d2e419859dce
# Updated: February 3, 2021
#

set -e
set -o pipefail

readonly SCRIPT_NAME=$(basename "${0}")

IS_DRYRUN=true

# hat tip: https://github.com/lhunath/scripts/blob/master/bashlib/bashcomplib#L98
show_args() {
    echo
    local i=0
    for arg; do
        printf "arg %d: %s\n" "$((i++))" "$arg"
    done

    i=0
    for word in "${COMP_WORDS[@]}"; do
        printf "word %d: %s -> %s %s\n" "$i" "$word" "$(xargs <<< "$word")" "$( ((i == "$COMP_CWORD")) && echo '<CWORD>')"
        ((i++))
    done
}

die() {
    printf >&2 '%s\n' "$*"
    exit 1
}

show_usage() {
    echo "Usage: ${SCRIPT_NAME} [option]"
    echo
    echo "Delete dotnet build dirs and files (e.g. 'bin', 'obj', etc...)"
    echo "recursively, starting from the current working directory (PWD)."
    echo
    echo "Options:"
    echo
    echo "  -k, --kill-dotnet-procs   killall(1) dotnet processes"
    echo "  -n, --dry-run             preview without delete."
    echo "  -h, --help                show this help message and exit."
    echo ""
    echo "Note: Only one option is allowed per execution."
    echo
}

heading() { printf '→ %s\n' "$@"; }
warn() { printf "$(tput setaf 3)%s$(tput sgr0)\\n" "$@"; }
success() { printf "$(tput setaf 76)✔ %s$(tput sgr0)\\n" "$@"; }

function killall_dotnet_processes() {
    heading "Killing any running dotnet processes..."
    # temporarily allow failures from killall(1)
    set +e
    killall dotnet
    set -e
}

clean_dirs_recursively() {
    heading "Cleaning '${1}' dirs..."
    find . -name "${1}" -type d -print | xargs -0 echo
    if [ "$IS_DRYRUN" = "false" ]; then
        # shellcheck disable=SC2086,SC2048
        find . -name ${1} -type d -print0 | xargs -0 rm -rf
    fi
}

clean_files_recursively() {
    heading "Cleaning '${1}' files..."
    find . -name "${1}" -type f -print | xargs -0 echo
    if [ "$IS_DRYRUN" = "false" ]; then
        # shellcheck disable=SC2086,SC2048
        find . -name ${1} -type f -print0 | xargs -0 rm -rf
    fi
}

main() {
    echo

    if [[ $# -gt 1 ]]; then
        local shown_args

        # shellcheck disable=SC2086,SC2048
        shown_args="$(show_args $*)"
        warn "Multiple options in not supported."
        echo "${shown_args}"
        echo ""
        die "See --help for usage."
    fi

    if [[ $1 == "-h" || $1 == "--help" || $1 == "help" || $1 == "-v" || $1 == "--version" ]]; then
        show_usage
        exit 0
    fi

    if [[ $1 == "-n" || $1 == "--dry-run" ]]; then
        IS_DRYRUN=true
        warn "Dry-run only... files will NOT be deleted."
        echo
    else
        IS_DRYRUN=false
    fi

    if [[ $1 == "-k" || $1 == "--kill-dotnet-procs" ]]; then
        killall_dotnet_processes
    fi

    #
    # clean vs dirs:
    clean_dirs_recursively 'bin'
    clean_dirs_recursively 'obj'

    # other dotnet ide user setttings dirs (visual studio, ryder, vscode)
    clean_dirs_recursively '.vs'
    clean_dirs_recursively '.idea'
    clean_dirs_recursively '.vscode'

    # careful with this one... pass '--dry-run' to preview without delete
    clean_dirs_recursively 'packages'

    # node_modules
    clean_dirs_recursively 'node_modules'

    # test results
    clean_dirs_recursively 'test-results'
    clean_dirs_recursively 'TestResults'
    clean_dirs_recursively 'coverlet'

    # published output
    clean_dirs_recursively 'publish'
    clean_dirs_recursively 'artifacts'

    #
    # legacy visual studio user settings files:
    clean_files_recursively '*.suo'

    #
    # vs and mono-develop user prefs
    clean_files_recursively '*.user'
    clean_files_recursively '*.userprefs'

    #
    # stylecop cache files:
    clean_files_recursively 'StyleCop.Cache'

    #
    # macOS files:
    clean_files_recursively '.DS_Store'

    echo
    success "Finished."
    echo
    exit 0
}

# shellcheck disable=SC2086,SC2048
main $*