aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/update.sh
blob: 0e3cf0e895ca256b00367b71cb5eb7475ba6bf58 (plain) (tree)
1
2
3
4
5
6
7
8


                                                        
                                                                
                                                               

                                    
                                                            










                                                                              
                                                                                          
 



               









                                                          

                      

                  










                                      
                                                                       




                    
                                              
 
                        
                                 
                                                          
 


                            
        

                                                                                                                    
 
 
                 
                     
 



                     

                    





                            

                                    
                  
                                

                          













                                        
               

                                                                       
                  





                               

                 

                                        


                                   
                                                      


            

                         



                                                         
                    






                                                                                     
                            
                                                                               

                                                      

 

                     


                       
                           
                                                 


                
                       
 




                                                  

                                                 

                
 
                                 
                                                    
 
 


                    
                                                         



                                                                   





                                                         
                                                                 


                                                              

                        

 
                           
 
                     
 
                     
                           
                                            


                
                        
                                                                   
 

                                                      
 
                                

 

                      

                                      

                   

                           
                                             


                
                                               
                                            

 
                          
                                                   


                 
                                                 
 


                                      
                     


                  
                          
 
                


                                      
                                               


        
                        
                         
                                                     


                

                          


                       

                                                                    
 









                                               
                                                                              

 
                           


                                      
                                   

                      
                                                                  
                                  
 
                             
                                                                       
                                                      
                                             
                                                      
                                                    
 





                                                                                 
 
                                             


                                                                          
 

                                                               
 


                                                                            
              
 



                                                         
                                                                          
 


                    

                                                     



                         
                                                           
 
                                                     
                                                       

                    
 

                                                


        
                        

                                               
                                                            

                      
                                                               
                                  
 

                                                    
                             

                                                                       
 

                                                
                                                         
 
                                                                       
 
                                                   
                                                        

                        
 

                                                             
 





                                                                            

                              
                                                          
 
                                                                                                                                       

 

               

                                                  
 
                
             




                                                                                                   

                                               

                                                           
                                                          








                            

                            
                  



                         
                                          
                  
              

                                                               



                               

                                                                   






                          
                                             
                  

                                          
                  
              

                                                               




                  



                          







                                     
        
                             
                          

                          
                           
                        


                  
         
#!/usr/bin/env bash

# Copyright (c) 2016 Egor Tensin <Egor.Tensin@gmail.com>
# This file is part of the "Configuration file sharing" project.
# For details, see https://github.com/egor-tensin/config-links.
# Distributed under the MIT License.

# This script relies on the availability of native symlinks.
# Those are indeed supported by NTFS, but require Administrator privileges for
# creation.
# It likely won't bother you as long as you don't use the functions defined in
# this file.
# In any case, you will see `ln` complaining about some access being denied in
# case something goes wrong.
#
# Remember that in order to force `ln` to use native NTFS symlinks, your
# `CYGWIN` Windows environment variable value **must** include either
# `winsymlinks:native` or `winsymlinks:nativestrict`!

# usage: ./update.sh [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-n|--dry-run]

set -o errexit
set -o nounset
set -o pipefail

dump() {
    local prefix="${FUNCNAME[0]}"
    [ "${#FUNCNAME[@]}" -gt 1 ] && prefix="${FUNCNAME[1]}"

    local msg
    for msg; do
        echo "$prefix: $msg"
    done
}

# Cygwin-related stuff

os="$( uname -o )"
readonly os

is_cygwin() {
    test "$os" == 'Cygwin'
}

check_symlinks_enabled_cygwin() {
    case "${CYGWIN-}" in
        *winsymlinks:native*)       ;;
        *winsymlinks:nativestrict*) ;;

        *)
            dump "native Windows symlinks aren't enabled in Cygwin" >&2
            return 1
            ;;
    esac
}

# Making sure paths point to files/directories

_traverse_path_usage() {
    local prefix="${FUNCNAME[0]}"
    [ "${#FUNCNAME[@]}" -gt 1 ] && prefix="${FUNCNAME[1]}"

    local msg
    for msg; do
        echo "$prefix: $msg"
    done

    echo "usage: $prefix [-h|--help] [-0|--null|-z|--zero] [-e|--exist] [-f|--file] [-d|--directory] [--] [PATH]..."
}

traverse_path() {
    local -a paths=()

    local must_exist=
    local type_flag=
    local type_name=

    local fmt='%s\n'

    while [ "$#" -gt 0 ]; do
        local key="$1"
        shift

        case "$key" in
            -h|--help)
                _traverse_path_usage
                return 0
                ;;
            -0|--null|-z|--zero)
                fmt='%s\0'
                ;;
            --)
                break
                ;;
            -e|--exist)
                must_exist=1
                ;;
            -d|--directory)
                type_flag=-d
                type_name="directory"
                ;;
            -f|--file)
                type_flag=-f
                type_name="regular file"
                ;;
            -*)
                _traverse_path_usage "unrecognized parameter: $key" >&2
                return 1
                ;;
            *)
                paths+=("$key")
                ;;
        esac
    done

    paths+=("$@")

    [ "${#paths[@]}" -eq 0 ] && return 0

    if is_cygwin; then
        local i
        for i in "${!paths[@]}"; do
            paths[$i]="$( cygpath -- "${paths[$i]}" )"
        done
    fi

    local -a abs_paths=()

    local path
    while IFS= read -d '' -r path; do
        if [ -n "$must_exist" ] && [ ! -e "$path" ]; then
            dump "must exist: $path" >&2
            return 1
        fi

        if [ -e "$path" ] && [ -n "$type_flag" ] && ! test "$type_flag" "$path"; then
            dump "must be a $type_name: $path" >&2
            return 1
        fi

        abs_paths+=("$path")
    done < <( readlink -z --canonicalize-missing -- ${paths[@]+"${paths[@]}"} )

    printf -- "$fmt" ${abs_paths[@]+"${abs_paths[@]}"}
}

# Variable resolution

declare -A cached_paths

resolve_variable() {
    if [ "$#" -ne 1 ]; then
        echo "usage: ${FUNCNAME[0]} VAR_NAME" >&2
        return 1
    fi

    local var_name="$1"

    if [ -n "${cached_paths[$var_name]+x}" ]; then
        echo "${cached_paths[$var_name]}"
        return 0
    fi

    if [ -z "${!var_name+x}" ]; then
        dump "variable is not set: $var_name" >&2
        return 1
    fi

    local var_path="${!var_name}"
    traverse_path --exist --directory -- "$var_path"
}

cache_variable() {
    local var_name
    for var_name; do
        [ -n "${cached_paths[$var_name]+x}" ] && continue
        cached_paths[$var_name]="$( resolve_variable "$var_name" )"
    done
}

readonly var_name_regex='%\([_[:alpha:]][_[:alnum:]]*\)%'

extract_variable_name() {
    local s
    for s; do
        local var_name
        if ! var_name="$( expr "$s" : "$var_name_regex/" )"; then
            dump "couldn't extract variable name from: $s" >&2
            return 1
        fi
        echo "$var_name"
    done
}

# Shared directory settings

shared_dir="$( pwd )"

update_shared_dir() {
    if [ "$#" -ne 1 ]; then
        echo "usage: ${FUNCNAME[0]} DIR" >&2
        return 1
    fi

    local new_shared_dir
    new_shared_dir="$( traverse_path --exist --directory -- "$1" )"

    [ "$db_path" == "$shared_dir/$default_db_name" ] \
        && db_path="$new_shared_dir/$default_db_name"

    shared_dir="$new_shared_dir"
}

# Database maintenance

readonly default_db_name='links.bin'
db_path="$shared_dir/$default_db_name"
declare -A database

update_database_path() {
    if [ "$#" -ne 1 ]; then
        echo "usage: ${FUNCNAME[0]} PATH" >&2
        return 1
    fi

    db_path="$( traverse_path --file -- "$1" )"
    mkdir -p -- "$( dirname -- "$db_path" )"
}

ensure_database_exists() {
    [ -f "$db_path" ] || is_dry_run || > "$db_path"
}

read_database() {
    [ ! -f "$db_path" ] && is_dry_run && return 0

    local entry
    while IFS= read -d '' -r entry; do
        database[$entry]=1
    done < "$db_path"
}

write_database() {
    is_dry_run && return 0

    > "$db_path"

    local entry
    for entry in "${!database[@]}"; do
        printf -- '%s\0' "$entry" >> "$db_path"
    done
}

delete_obsolete_dirs() {
    if [ $# -ne 2 ]; then
        echo "usage: ${FUNCNAME[0]} BASE_DIR DIR" >&2
        return 1
    fi

    is_dry_run && return 0

    local base_dir="$1"
    local dir="$2"

    base_dir="$( traverse_path --exist --directory -- "$base_dir" )"
    dir="$( traverse_path --exist --directory -- "$dir" )"

    [ "$base_dir" == "$dir" ] && return 0

    local subpath="${dir##$base_dir/}"

    if [ "$subpath" == "$dir" ]; then
        dump "base directory: $base_dir"    >&2
        dump "... is not a parent of: $dir" >&2
        return 1
    fi

    ( cd -- "$base_dir" && rmdir -p --ignore-fail-on-non-empty -- "$subpath" )
}

delete_obsolete_entries() {
    local entry
    for entry in "${!database[@]}"; do
        dump "entry: $entry"
        unset -v 'database[$entry]'

        local var_name
        var_name="$( extract_variable_name "$entry" )" || continue
        cache_variable "$var_name"

        local symlink_var_dir
        symlink_var_dir="$( resolve_variable "$var_name" )" || continue
        local shared_var_dir="$shared_dir/%$var_name%"
        local subpath="${entry#%$var_name%/}"
        local symlink_path="$symlink_var_dir/$subpath"
        local shared_path="$shared_var_dir/$subpath"

        dump "    shared file path: $shared_path"
        dump "    symlink path: $symlink_path"

        if [ ! -L "$shared_path" ] && [ ! -e "$shared_path" ]; then
            dump '    the shared file is missing, so going to delete the symlink'
            is_dry_run && continue

            if [ ! -L "$symlink_path" ]; then
                dump "    not a symlink or doesn't exist, so won't delete"
                continue
            fi

            local target_path
            target_path="$( traverse_path -- "$symlink_path" )"

            if [ "$shared_path" != "$target_path" ]; then
                dump "    doesn't point to the shared file, so won't delete"
                continue
            fi

            rm -f -- "$symlink_path"

            local symlink_dir
            symlink_dir="$( dirname -- "$symlink_path" )"
            delete_obsolete_dirs "$symlink_var_dir" "$symlink_dir" || true

            continue
        fi

        if [ ! -L "$symlink_path" ]; then
            dump "    not a symlink or doesn't exist"
            continue
        fi

        local target_path
        target_path="$( traverse_path -- "$symlink_path" )"

        if [ "$target_path" != "$shared_path" ]; then
            dump "    doesn't point to the shared file"
            continue
        fi

        dump '    ... points to the shared file'
        database[$entry]=1
    done
}

discover_new_entries() {
    local shared_var_dir
    while IFS= read -d '' -r shared_var_dir; do
        dump "shared directory: $shared_dir/$shared_var_dir"

        local var_name
        var_name="$( extract_variable_name "$shared_var_dir" )"
        cache_variable "$var_name"

        shared_var_dir="$shared_dir/$shared_var_dir"

        local symlink_var_dir
        symlink_var_dir="$( resolve_variable "$var_name" )" || continue
        dump "    symlinks directory: $symlink_var_dir"

        local shared_path
        while IFS= read -d '' -r shared_path; do
            dump "        shared file path: $shared_path"

            local entry="%$var_name%/${shared_path:${#shared_var_dir}}"

            if [ -n "${database[$entry]+x}" ]; then
                dump '        ... already has a symlink'
                continue
            fi

            local subpath="${shared_path:${#shared_var_dir}}"
            local symlink_path="$symlink_var_dir/$subpath"

            dump "        symlink path: $symlink_path"

            is_dry_run && continue

            mkdir -p -- "$( dirname -- "$symlink_path" )"
            ln -f -s --no-target-directory -- "$shared_path" "$symlink_path"

            database[$entry]=1
        done < <( find "$shared_var_dir" -type f -print0 )

    done < <( find "$shared_dir" -regextype posix-basic -mindepth 1 -maxdepth 1 -type d -regex ".*/$var_name_regex\$" -printf '%P/\0' )
}

# Main routines

script_name="$( basename -- "${BASH_SOURCE[0]}" )"
readonly script_name

script_usage() {
    local msg
    for msg; do
        echo "$script_name: $msg"
    done

    echo "usage: $script_name [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-n|--dry-run]
  -h,--help          show this message and exit
  -d,--database      set database file path
  -s,--shared-dir    set top-level shared directory path
                     (current working directory by default)
  -n,--dry-run       don't actually do anything intrusive"
}

parse_script_options() {
    while [ "$#" -gt 0 ]; do
        local key="$1"
        shift

        case "$key" in
            -h|--help)
                script_usage
                exit 0
                ;;
            -n|--dry-run)
                dry_run=1
                continue
                ;;
            -d|--database|-s|--shared-dir)
                ;;
            *)
                script_usage "unrecognized parameter: $key" >&2
                exit 1
                ;;
        esac

        if [ "$#" -eq 0 ]; then
            script_usage "missing argument for parameter: $key" >&2
            exit 1
        fi

        local value="$1"
        shift

        case "$key" in
            -d|--database)
                update_database_path "$value"
                ;;
            -s|--shared-dir)
                update_shared_dir "$value"
                ;;
            *)
                script_usage "unrecognized parameter: $key" >&2
                exit 1
                ;;
        esac
    done
}

is_dry_run() {
    test -n "${dry_run+x}"
}

check_symlinks_enabled() {
    if is_cygwin; then
        check_symlinks_enabled_cygwin
    else
        return 0
    fi
}

main() {
    parse_script_options "$@"
    check_symlinks_enabled
    ensure_database_exists
    read_database
    delete_obsolete_entries
    discover_new_entries
    write_database
}

main "$@"