summaryrefslogtreecommitdiffstatshomepage
path: root/lib
diff options
context:
space:
mode:
authorEgor Tensin <egor@tensin.name>2026-06-06 18:12:33 +0200
committerEgor Tensin <egor@tensin.name>2026-06-06 18:17:34 +0200
commit40b7b047a3fbf6451228de7f69c8ad5f156f7d1c (patch)
tree365df06d7b7cb6122d8a30695aa45a12c4f58302 /lib
parentworkflows/ci: remove on: pull_request (diff)
downloadconfig-links-40b7b047a3fbf6451228de7f69c8ad5f156f7d1c.tar.gz
config-links-40b7b047a3fbf6451228de7f69c8ad5f156f7d1c.zip
reorganize filesv2.1.0
Diffstat (limited to 'lib')
-rw-r--r--lib/common.sh22
-rw-r--r--lib/db.sh308
-rw-r--r--lib/os.sh30
-rw-r--r--lib/path.sh94
-rw-r--r--lib/vars.sh59
5 files changed, 513 insertions, 0 deletions
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 <egor@tensin.name>
+# 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 <egor@tensin.name>
+# 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 <egor@tensin.name>
+# 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 <egor@tensin.name>
+# 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 <egor@tensin.name>
+# 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
+}