From 40b7b047a3fbf6451228de7f69c8ad5f156f7d1c Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 6 Jun 2026 18:12:33 +0200 Subject: reorganize files --- lib/common.sh | 22 +++++ lib/db.sh | 308 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/os.sh | 30 ++++++ lib/path.sh | 94 ++++++++++++++++++ lib/vars.sh | 59 +++++++++++ 5 files changed, 513 insertions(+) create mode 100644 lib/common.sh create mode 100644 lib/db.sh create mode 100644 lib/os.sh create mode 100644 lib/path.sh create mode 100644 lib/vars.sh (limited to 'lib') diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..51c9827 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,22 @@ +# Copyright (c) 2016 Egor Tensin +# This file is part of the "Config file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +dump() { + local prefix="${FUNCNAME[0]}" + [ "${#FUNCNAME[@]}" -gt 1 ] && prefix="${FUNCNAME[1]}" + + local msg + for msg; do + echo "$prefix: $msg" + done +} + +set_dry_run() { + dry_run=1 +} + +is_dry_run() { + test -n "${dry_run+x}" +} diff --git a/lib/db.sh b/lib/db.sh new file mode 100644 index 0000000..ae96487 --- /dev/null +++ b/lib/db.sh @@ -0,0 +1,308 @@ +# Copyright (c) 2016 Egor Tensin +# This file is part of the "Config file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Shared directory settings + +shared_root_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" )" + + [ "$new_shared_dir" = / ] || new_shared_dir="$new_shared_dir/" + + [ "$db_path" = "$shared_root_dir$default_db_name" ] \ + && db_path="$new_shared_dir$default_db_name" + + shared_root_dir="$new_shared_dir" +} + +# Database maintenance + +readonly default_db_name='links.bin' +db_path="$shared_root_dir$default_db_name" +declare -A database=() +declare -A shared_paths=() +declare -A symlink_paths=() + +update_database_path() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} PATH" >&2 + return 1 + fi + + db_path="$( traverse_path --file -- "$1" )" + + local db_dir + db_dir="$( dirname -- "$db_path" )" + mkdir -p -- "$db_dir" +} + +add_entry() { + local entry + for entry; do + dump "entry: $entry" + + local var_name + var_name="$( extract_variable_name "$entry" )" + cache_variable "$var_name" + + local shared_var_dir="$shared_root_dir%$var_name%" + local symlink_var_dir + symlink_var_dir="$( resolve_variable "$var_name" )" + local subpath="${entry#%"$var_name"%/}" + + local shared_path="$shared_var_dir" + [ "$shared_var_dir" != / ] && shared_path="$shared_path/" + shared_path="$shared_path$subpath" + + local symlink_path="$symlink_var_dir" + [ "$symlink_var_dir" != / ] && symlink_path="$symlink_path/" + symlink_path="$symlink_path$subpath" + + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + database[$entry]="$var_name" + shared_paths[$entry]="$shared_path" + symlink_paths[$entry]="$symlink_path" + done +} + +remove_entry() { + local entry + for entry; do + unset -v 'database[$entry]' + unset -v 'shared_paths[$entry]' + unset -v 'symlink_paths[$entry]' + done +} + +read_database() { + [ ! -r "$db_path" ] && return 0 + + local entry + while IFS= read -d '' -r entry; do + add_entry "$entry" + done < "$db_path" +} + +write_database() { + is_dry_run && return 0 + + [ "${#database[@]}" -eq 0 ] && rm -f -- "$db_path" && return 0 + + > "$db_path" + + local entry + for entry in "${!database[@]}"; do + printf -- '%s\0' "$entry" >> "$db_path" + done +} + +link_entry() { + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + + local symlink_dir + symlink_dir="$( dirname -- "$symlink_path" )" + mkdir -p -- "$symlink_dir" + ln -f -s --no-target-directory -- "$shared_path" "$symlink_path" + done +} + +delete_obsolete_dirs() { + if [ "$#" -ne 2 ]; then + echo "usage: ${FUNCNAME[0]} BASE_DIR DIR" >&2 + return 1 + fi + + 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" ) +} + +unlink_entry() { + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + + rm -f -- "$symlink_path" + + local symlink_dir + symlink_dir="$( dirname -- "$symlink_path" )" + local var_name="${database[$entry]}" + local symlink_var_dir + symlink_var_dir="$( resolve_variable "$var_name" )" + delete_obsolete_dirs "$symlink_var_dir" "$symlink_dir" || true + done +} + +symlink_present() { + local entry + for entry; do + local symlink_path="${symlink_paths[$entry]}" + test -L "$symlink_path" -o -e "$symlink_path" + done +} + +symlink_points_to_shared_file() { + symlink_present "$@" + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + shared_path="$( traverse_path -- "$shared_path" )" + local symlink_path="${symlink_paths[$entry]}" + local target_path + target_path="$( traverse_path -- "$symlink_path" )" + test "$target_path" = "$shared_path" + done +} + +shared_file_present() { + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + test -L "$shared_path" -o -e "$shared_path" + done +} + +link_all_entries() { + local -a shared_var_dirs=() + local shared_var_dir + + find "$shared_root_dir" \ + -mindepth 1 -maxdepth 1 \ + -\( -type d -o -type l -\) \ + -regextype posix-basic \ + -regex ".*/$var_name_regex\$" \ + -printf '%P\0' | + while IFS= read -d '' -r shared_var_dir; do + shared_var_dirs+=("$shared_var_dir") + done + + for shared_var_dir in ${shared_var_dirs[@]+"${shared_var_dirs[@]}"}; do + dump "shared directory: $shared_root_dir$shared_var_dir" + local shared_path + + find "$shared_root_dir$shared_var_dir/" -\( -type f -o -type l -\) -print0 | + while IFS= read -d '' -r shared_path; do + dump " shared file path: $shared_path" + local entry="${shared_path:${#shared_root_dir}}" + add_entry "$entry" > /dev/null + dump " symlink path: ${symlink_paths[$entry]}" + + if symlink_present "$entry"; then + if symlink_points_to_shared_file "$entry"; then + dump ' ... up-to-date' + else + dump " ... not a symlink or doesn't point to the shared file, adding a symlink" + is_dry_run || link_entry "$entry" + fi + else + dump ' ... adding a symlink' + is_dry_run || link_entry "$entry" + fi + done + done +} + +unlink_all_entries() { + local entry + for entry in "${!database[@]}"; do + dump "entry: $entry" + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + if symlink_points_to_shared_file "$entry"; then + dump ' ... removing the symlink' + is_dry_run || unlink_entry "$entry" + remove_entry "$entry" + else + dump " ... not a symlink or doesn't point to the shared file" + remove_entry "$entry" + fi + done +} + +unlink_obsolete_entries() { + local entry + for entry in "${!database[@]}"; do + dump "entry: $entry" + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + if symlink_points_to_shared_file "$entry"; then + if shared_file_present "$entry"; then + dump ' ... up-to-date' + else + dump ' ... obsolete' + is_dry_run || unlink_entry "$entry" + remove_entry "$entry" + fi + else + dump " ... not a symlink or doesn't point to the shared file" + remove_entry "$entry" + fi + done +} + +parse_mode() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} MODE" >&2 + return 1 + fi + + local mode="$1" + shift + + if [ -z "$mode" ]; then + dump "mode cannot be empty" >&2 + return 1 + fi + + echo "$mode" +} + +chmod_entries() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} MODE" >&2 + return 1 + fi + + local mode="$1" + shift + + dump "applying mode $mode to shared files..." + if [ "${#shared_paths[@]}" -ne 0 ]; then + is_dry_run || chmod -- "$mode" ${shared_paths[@]+"${shared_paths[@]}"} + fi +} diff --git a/lib/os.sh b/lib/os.sh new file mode 100644 index 0000000..01d70f4 --- /dev/null +++ b/lib/os.sh @@ -0,0 +1,30 @@ +# Copyright (c) 2016 Egor Tensin +# This file is part of the "Config file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Mostly Cygwin-related stuff + +is_cygwin() { + test "$OSTYPE" == 'cygwin' +} + +check_symlinks_enabled_cygwin() { + case "${CYGWIN-}" in + *winsymlinks:nativestrict*) ;; + *winsymlinks:native*) ;; + + *) + dump "native Windows symlinks aren't enabled in Cygwin" >&2 + return 1 + ;; + esac +} + +check_symlinks_enabled() { + if is_cygwin; then + check_symlinks_enabled_cygwin + else + return 0 + fi +} diff --git a/lib/path.sh b/lib/path.sh new file mode 100644 index 0000000..4fcdb94 --- /dev/null +++ b/lib/path.sh @@ -0,0 +1,94 @@ +# Copyright (c) 2016 Egor Tensin +# This file is part of the "Config file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# 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 + readlink -z --canonicalize-missing -- ${paths[@]+"${paths[@]}"} | 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 + + printf -- "$fmt" ${abs_paths[@]+"${abs_paths[@]}"} +} diff --git a/lib/vars.sh b/lib/vars.sh new file mode 100644 index 0000000..97b8da9 --- /dev/null +++ b/lib/vars.sh @@ -0,0 +1,59 @@ +# Copyright (c) 2016 Egor Tensin +# This file is part of the "Config file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Variable resolution + +declare -A cached_paths + +readonly root_var_name='CONFIG_LINKS_ROOT' + +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 [ "$var_name" = "$root_var_name" ]; then + echo '/' + 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 +} -- cgit v1.3