From 40b7b047a3fbf6451228de7f69c8ad5f156f7d1c Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 6 Jun 2026 18:12:33 +0200 Subject: reorganize files --- .gitattributes | 2 +- bin/links-chmod | 115 ++++++++++++++++++ bin/links-remove | 95 +++++++++++++++ bin/links-update | 104 ++++++++++++++++ lib/common.sh | 22 ++++ lib/db.sh | 308 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/os.sh | 30 +++++ lib/path.sh | 94 +++++++++++++++ lib/vars.sh | 59 ++++++++++ links-chmod | 115 ------------------ links-remove | 95 --------------- links-update | 104 ---------------- src/common.sh | 22 ---- src/db.sh | 308 ------------------------------------------------ src/os.sh | 30 ----- src/path.sh | 94 --------------- src/vars.sh | 59 ---------- test/linux-home/test.sh | 6 +- test/local/test.sh | 6 +- 19 files changed, 834 insertions(+), 834 deletions(-) create mode 100755 bin/links-chmod create mode 100755 bin/links-remove create mode 100755 bin/links-update 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 delete mode 100755 links-chmod delete mode 100755 links-remove delete mode 100755 links-update delete mode 100644 src/common.sh delete mode 100644 src/db.sh delete mode 100644 src/os.sh delete mode 100644 src/path.sh delete mode 100644 src/vars.sh diff --git a/.gitattributes b/.gitattributes index 78efe19..2aef5b2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,7 @@ *.sh text eol=lf -/links-* text eol=lf +/bin/links-* text eol=lf /.github/ export-ignore .gitattributes export-ignore diff --git a/bin/links-chmod b/bin/links-chmod new file mode 100755 index 0000000..931a5b5 --- /dev/null +++ b/bin/links-chmod @@ -0,0 +1,115 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021 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. + +# usage: ./links-chmod [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-n|--dry-run] MODE + +set -o errexit -o nounset -o pipefail +shopt -s inherit_errexit 2> /dev/null || true +shopt -s lastpipe + +script_name="$( basename -- "${BASH_SOURCE[0]}" )" +readonly script_name +script_path="$( readlink --canonicalize-existing -- "${BASH_SOURCE[0]}" )" +readonly script_path +script_dir="$( dirname -- "$script_path" )" +readonly script_dir +lib_dir="$( cd -- "$script_dir/../lib" && pwd )" +readonly lib_dir + +. "$lib_dir/common.sh" +. "$lib_dir/os.sh" +. "$lib_dir/path.sh" +. "$lib_dir/vars.sh" +. "$lib_dir/db.sh" + +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] MODE + MODE shared files mode (as in chmod) + -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() { + local -a args=() + + while [ "$#" -gt 0 ]; do + local key="$1" + shift + + case "$key" in + -h|--help) + script_usage + exit 0 + ;; + -n|--dry-run) + set_dry_run + continue + ;; + -d|--database|-s|--shared-dir) + ;; + --) + break + ;; + -*) + script_usage "unrecognized parameter: $key" >&2 + exit 1 + ;; + *) + args+=("$key") + continue + ;; + 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 + + args+=("$@") + + if [ "${#args[@]}" -ne 1 ]; then + script_usage "missing parameter MODE" >&2 + exit 1 + fi + + mode="$( parse_mode "${args[0]}" )" +} + +mode='' + +main() { + parse_script_options "$@" + read_database + chmod_entries "$mode" +} + +main "$@" diff --git a/bin/links-remove b/bin/links-remove new file mode 100755 index 0000000..a149e52 --- /dev/null +++ b/bin/links-remove @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Copyright (c) 2019 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. + +# usage: ./links-remove [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-n|--dry-run] + +set -o errexit -o nounset -o pipefail +shopt -s inherit_errexit 2> /dev/null || true +shopt -s lastpipe + +script_name="$( basename -- "${BASH_SOURCE[0]}" )" +readonly script_name +script_path="$( readlink --canonicalize-existing -- "${BASH_SOURCE[0]}" )" +readonly script_path +script_dir="$( dirname -- "$script_path" )" +readonly script_dir +lib_dir="$( cd -- "$script_dir/../lib" && pwd )" +readonly lib_dir + +. "$lib_dir/common.sh" +. "$lib_dir/os.sh" +. "$lib_dir/path.sh" +. "$lib_dir/vars.sh" +. "$lib_dir/db.sh" + +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) + set_dry_run + 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 +} + +main() { + parse_script_options "$@" + read_database + unlink_all_entries + write_database +} + +main "$@" diff --git a/bin/links-update b/bin/links-update new file mode 100755 index 0000000..dac40ff --- /dev/null +++ b/bin/links-update @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +# 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. + +# usage: ./links-update [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-m|--mode MODE] [-n|--dry-run] + +set -o errexit -o nounset -o pipefail +shopt -s inherit_errexit 2> /dev/null || true +shopt -s lastpipe + +script_name="$( basename -- "${BASH_SOURCE[0]}" )" +readonly script_name +script_path="$( readlink --canonicalize-existing -- "${BASH_SOURCE[0]}" )" +readonly script_path +script_dir="$( dirname -- "$script_path" )" +readonly script_dir +lib_dir="$( cd -- "$script_dir/../lib" && pwd )" +readonly lib_dir + +. "$lib_dir/common.sh" +. "$lib_dir/os.sh" +. "$lib_dir/path.sh" +. "$lib_dir/vars.sh" +. "$lib_dir/db.sh" + +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] [-m|--mode MODE] [-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) + -m,--mode shared files mode (as in chmod) + -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) + set_dry_run + continue + ;; + -d|--database|-s|--shared-dir|-m|--mode) + ;; + *) + 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" + ;; + -m|--mode) + mode="$( parse_mode "$value" )" + ;; + *) + script_usage "unrecognized parameter: $key" >&2 + exit 1 + ;; + esac + done +} + +mode='' + +main() { + parse_script_options "$@" + check_symlinks_enabled + read_database + unlink_obsolete_entries + link_all_entries + test -n "$mode" && chmod_entries "$mode" + write_database +} + +main "$@" 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 +} diff --git a/links-chmod b/links-chmod deleted file mode 100755 index 167cb8e..0000000 --- a/links-chmod +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021 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. - -# usage: ./links-chmod [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-n|--dry-run] MODE - -set -o errexit -o nounset -o pipefail -shopt -s inherit_errexit 2> /dev/null || true -shopt -s lastpipe - -script_name="$( basename -- "${BASH_SOURCE[0]}" )" -readonly script_name -script_path="$( readlink --canonicalize-existing -- "${BASH_SOURCE[0]}" )" -readonly script_path -script_dir="$( dirname -- "$script_path" )" -readonly script_dir -src_dir="$( cd -- "$script_dir/src" && pwd )" -readonly src_dir - -. "$src_dir/common.sh" -. "$src_dir/os.sh" -. "$src_dir/path.sh" -. "$src_dir/vars.sh" -. "$src_dir/db.sh" - -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] MODE - MODE shared files mode (as in chmod) - -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() { - local -a args=() - - while [ "$#" -gt 0 ]; do - local key="$1" - shift - - case "$key" in - -h|--help) - script_usage - exit 0 - ;; - -n|--dry-run) - set_dry_run - continue - ;; - -d|--database|-s|--shared-dir) - ;; - --) - break - ;; - -*) - script_usage "unrecognized parameter: $key" >&2 - exit 1 - ;; - *) - args+=("$key") - continue - ;; - 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 - - args+=("$@") - - if [ "${#args[@]}" -ne 1 ]; then - script_usage "missing parameter MODE" >&2 - exit 1 - fi - - mode="$( parse_mode "${args[0]}" )" -} - -mode='' - -main() { - parse_script_options "$@" - read_database - chmod_entries "$mode" -} - -main "$@" diff --git a/links-remove b/links-remove deleted file mode 100755 index 2941509..0000000 --- a/links-remove +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2019 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. - -# usage: ./links-remove [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-n|--dry-run] - -set -o errexit -o nounset -o pipefail -shopt -s inherit_errexit 2> /dev/null || true -shopt -s lastpipe - -script_name="$( basename -- "${BASH_SOURCE[0]}" )" -readonly script_name -script_path="$( readlink --canonicalize-existing -- "${BASH_SOURCE[0]}" )" -readonly script_path -script_dir="$( dirname -- "$script_path" )" -readonly script_dir -src_dir="$( cd -- "$script_dir/src" && pwd )" -readonly src_dir - -. "$src_dir/common.sh" -. "$src_dir/os.sh" -. "$src_dir/path.sh" -. "$src_dir/vars.sh" -. "$src_dir/db.sh" - -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) - set_dry_run - 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 -} - -main() { - parse_script_options "$@" - read_database - unlink_all_entries - write_database -} - -main "$@" diff --git a/links-update b/links-update deleted file mode 100755 index 68910a2..0000000 --- a/links-update +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash - -# 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. - -# usage: ./links-update [-h|--help] [-d|--database PATH] [-s|--shared-dir DIR] [-m|--mode MODE] [-n|--dry-run] - -set -o errexit -o nounset -o pipefail -shopt -s inherit_errexit 2> /dev/null || true -shopt -s lastpipe - -script_name="$( basename -- "${BASH_SOURCE[0]}" )" -readonly script_name -script_path="$( readlink --canonicalize-existing -- "${BASH_SOURCE[0]}" )" -readonly script_path -script_dir="$( dirname -- "$script_path" )" -readonly script_dir -src_dir="$( cd -- "$script_dir/src" && pwd )" -readonly src_dir - -. "$src_dir/common.sh" -. "$src_dir/os.sh" -. "$src_dir/path.sh" -. "$src_dir/vars.sh" -. "$src_dir/db.sh" - -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] [-m|--mode MODE] [-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) - -m,--mode shared files mode (as in chmod) - -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) - set_dry_run - continue - ;; - -d|--database|-s|--shared-dir|-m|--mode) - ;; - *) - 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" - ;; - -m|--mode) - mode="$( parse_mode "$value" )" - ;; - *) - script_usage "unrecognized parameter: $key" >&2 - exit 1 - ;; - esac - done -} - -mode='' - -main() { - parse_script_options "$@" - check_symlinks_enabled - read_database - unlink_obsolete_entries - link_all_entries - test -n "$mode" && chmod_entries "$mode" - write_database -} - -main "$@" diff --git a/src/common.sh b/src/common.sh deleted file mode 100644 index 51c9827..0000000 --- a/src/common.sh +++ /dev/null @@ -1,22 +0,0 @@ -# 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/src/db.sh b/src/db.sh deleted file mode 100644 index ae96487..0000000 --- a/src/db.sh +++ /dev/null @@ -1,308 +0,0 @@ -# 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/src/os.sh b/src/os.sh deleted file mode 100644 index 01d70f4..0000000 --- a/src/os.sh +++ /dev/null @@ -1,30 +0,0 @@ -# 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/src/path.sh b/src/path.sh deleted file mode 100644 index 4fcdb94..0000000 --- a/src/path.sh +++ /dev/null @@ -1,94 +0,0 @@ -# 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/src/vars.sh b/src/vars.sh deleted file mode 100644 index 97b8da9..0000000 --- a/src/vars.sh +++ /dev/null @@ -1,59 +0,0 @@ -# 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 -} diff --git a/test/linux-home/test.sh b/test/linux-home/test.sh index edb2d5b..5021048 100755 --- a/test/linux-home/test.sh +++ b/test/linux-home/test.sh @@ -55,15 +55,15 @@ call_bin_script() { } call_update() { - call_bin_script "$root_dir/links-update" "$@" + call_bin_script "$root_dir/bin/links-update" "$@" } call_remove() { - call_bin_script "$root_dir/links-remove" + call_bin_script "$root_dir/bin/links-remove" } call_chmod() { - call_bin_script "$root_dir/links-chmod" "$@" + call_bin_script "$root_dir/bin/links-chmod" "$@" } test_my_dotfiles_work() { diff --git a/test/local/test.sh b/test/local/test.sh index 99b426f..1e8567f 100755 --- a/test/local/test.sh +++ b/test/local/test.sh @@ -66,15 +66,15 @@ call_bin_script() { } call_update() { - call_bin_script "$root_dir/links-update" "$@" + call_bin_script "$root_dir/bin/links-update" "$@" } call_remove() { - call_bin_script "$root_dir/links-remove" + call_bin_script "$root_dir/bin/links-remove" } call_chmod() { - call_bin_script "$root_dir/links-chmod" "$@" + call_bin_script "$root_dir/bin/links-chmod" "$@" } verify_output() { -- cgit v1.3