|
1 | 1 | #!/usr/bin/env bash |
| 2 | +set -Eeu -o pipefail |
2 | 3 |
|
3 | 4 | # Look through the local filesystem and exclude development dependencies |
4 | 5 | # from Apple Time Machine backups. |
|
12 | 13 | # |
13 | 14 | # For a full explanation, please see https://apple.stackexchange.com/a/25833/206772 |
14 | 15 | # |
15 | | -# @version 0.2.0 |
| 16 | +# @version 0.3.0 |
16 | 17 | # @author Steve Grunwell |
17 | 18 | # @license MIT |
18 | 19 |
|
19 | | -readonly FILEPATHS=( |
20 | | - "vendor ../composer.json" |
21 | | - "node_modules ../package.json" |
22 | | - ".vagrant ../Vagrantfile" |
| 20 | +readonly ASIMOV_ROOT=~ |
| 21 | + |
| 22 | +# Paths to unconditionally skip over. This prevents Asimov from modifying the |
| 23 | +# Time Machine exclusions for these paths (and decendents). It has an important |
| 24 | +# side-effect of speeding up the search. |
| 25 | +readonly ASIMOV_SKIP_PATHS=( |
| 26 | + ~/.Trash |
| 27 | + ~/Library |
23 | 28 | ) |
24 | 29 |
|
25 | | -# Given a directory path, determine if the corresponding file (relative |
26 | | -# to that directory) is available. |
| 30 | +# A list of "directory"/"sentinel" pairs. |
27 | 31 | # |
28 | | -# For example, when looking at a /vendor directory, we may choose to |
29 | | -# ensure a composer.json file is available. |
30 | | -dependency_file_exists() { |
31 | | - filename=$1 |
32 | | - |
33 | | - read -r path; |
34 | | - |
35 | | - while read -r path; do |
36 | | - |
37 | | - # Return early if this is a nested dependency (e.g. node_modules |
38 | | - # inside another node_modules directory. |
39 | | - if [[ $(dirname "$path") == *"/$(basename "$path")/"* ]]; then |
40 | | - continue; |
41 | | - fi |
42 | | - |
43 | | - if [ -f "${path}/${filename}" ]; then |
44 | | - echo "$path" |
45 | | - fi |
46 | | - done |
47 | | -} |
| 32 | +# Directories will only be excluded if the dependency ("sentinel") file exists. |
| 33 | +# |
| 34 | +# For example, 'node_modules package.json' means "exclude node_modules/ from the |
| 35 | +# Time Machine backups if there is a package.json file next to it." |
| 36 | +readonly ASIMOV_VENDOR_DIR_SENTINELS=( |
| 37 | + '.build Package.swift' # Swift |
| 38 | + '.packages pubspec.yaml' # Pub (Dart) |
| 39 | + '.stack-work stack.yaml' # Stack (Haskell) |
| 40 | + '.vagrant Vagrantfile' # Vagrant |
| 41 | + 'Carthage Cartfile' # Carthage |
| 42 | + 'Pods Podfile' # CocoaPods |
| 43 | + 'bower_components bower.json' # Bower (JavaScript) |
| 44 | + 'node_modules package.json' # npm, Yarn (NodeJS) |
| 45 | + 'target Cargo.toml' # Cargo (Rust) |
| 46 | + 'target pom.xml' # Maven |
| 47 | + 'vendor composer.json' # Composer (PHP) |
| 48 | + 'vendor Gemfile' # Bundler (Ruby) |
| 49 | +) |
48 | 50 |
|
49 | | -# Exclude the given path from Time Machine backups. |
| 51 | +# Exclude the given paths from Time Machine backups. |
| 52 | +# Reads the newline-separated list of paths from stdin. |
50 | 53 | exclude_file() { |
51 | | - while read -r path; do |
52 | | - if tmutil isexcluded "$path" | grep -q '\[Excluded\]'; then |
| 54 | + local path |
| 55 | + while IFS=$'\n' read -r path; do |
| 56 | + if tmutil isexcluded "${path}" | grep -Fq '[Excluded]'; then |
53 | 57 | echo "- ${path} is already excluded, skipping." |
54 | 58 | continue |
55 | 59 | fi |
56 | 60 |
|
57 | | - tmutil addexclusion "$path" |
| 61 | + tmutil addexclusion "${path}" |
58 | 62 |
|
59 | | - echo "- ${path} has been excluded from Time Machine backups." |
60 | | - done |
| 63 | + sizeondisk=$(du -hs "${path}" | cut -f1) |
| 64 | + echo "- ${path} has been excluded from Time Machine backups (${sizeondisk})." |
| 65 | + done |
61 | 66 | } |
62 | 67 |
|
63 | | -# Iterate over dependencies. |
64 | | -for i in "${FILEPATHS[@]}"; do |
65 | | - read -ra parts <<< "$i" |
| 68 | +# Iterate over the skip directories to construct the `find` expression. |
| 69 | +declare -a find_parameters_skip=() |
| 70 | +for d in "${ASIMOV_SKIP_PATHS[@]}"; do |
| 71 | + find_parameters_skip+=( -not \( -path "${d}" -prune \) ) |
| 72 | +done |
66 | 73 |
|
67 | | - printf "\\n\\033[0;36mFinding %s/ directories with corresponding %s files...\\033[0m\\n" \ |
68 | | - "${parts[0]}" "${parts[1]}" |
| 74 | +# Iterate over the directory/sentinel pairs to construct the `find` expression. |
| 75 | +declare -a find_parameters_vendor=() |
| 76 | +for i in "${ASIMOV_VENDOR_DIR_SENTINELS[@]}"; do |
| 77 | + read -ra parts <<< "${i}" |
69 | 78 |
|
70 | | - find ~ -name "${parts[0]}" -type d | dependency_file_exists "${parts[1]}" | exclude_file |
| 79 | + # Add this folder to the `find` list, allowing a single `find` command to find all |
| 80 | + _exclude_name="${parts[0]}" |
| 81 | + _sibling_sentinel_name="${parts[1]}" |
| 82 | + |
| 83 | + # Given a directory path, determine if the corresponding file (relative |
| 84 | + # to that directory) is available. |
| 85 | + # |
| 86 | + # For example, when looking at a /vendor directory, we may choose to |
| 87 | + # ensure a composer.json file is available. |
| 88 | + find_parameters_vendor+=( -or \( \ |
| 89 | + -type d \ |
| 90 | + -name "${_exclude_name}" \ |
| 91 | + -execdir test -e "${_sibling_sentinel_name}" \; \ |
| 92 | + -prune \ |
| 93 | + -print \ |
| 94 | + \) ) |
71 | 95 | done |
| 96 | + |
| 97 | +printf '\n\033[0;36mFinding dependency directories with corresponding definition files…\033[0m\n' |
| 98 | + |
| 99 | +find "${ASIMOV_ROOT}" \( "${find_parameters_skip[@]}" \) \( -false "${find_parameters_vendor[@]}" \) \ |
| 100 | + | exclude_file \ |
| 101 | + ; |
0 commit comments