# rln by David Laurence Emerson # version 0.01 # 2006-02-21 # # This bash script is insanely slow and would be well replaced by a c # implementation. # # rln stands for "relative (sym)link". It helps create symlinks by # intelligently finding an efficient relative path. # # Suppose you have a directory structure "/a/b/c/d/e". # Within "e" there are several files E1 E2 E3 E4 E5 E6. # Now, say you want quick access to several "E" files from within "/a" # One convenient way to achieve this is by making a few symlinks. # # It would be nice to "cd /a/b/c/d/e" and "ln -s E2 E4 E6 /a" # But that makes links like E4 -> E4 instead of E4 -> b/c/d/e/E4 # With "ln -s" you must explicitly specify each path relative to "/a", # necessitating many keystrokes: # "ln -s b/c/d/e/E2 b/c/d/e/E4 b/c/d/e/E6 /a" # (or something silly like # "for i in E2 E3 E4; do ln -s b/c/d/e/$i /a; done") # # rln enables use of the shorter syntax (described first). # The first clear benefit is that it enables use of tab-completion while # typing in a directory other than the destination directory. # Additionally, it lacks the ability to create dead links -- which may be # viewed as either a feature or a bug ;-) It uses "realpath" to help find # efficient relative paths between the "target" files to be linked to # and the "destination" where the symlink(s) will be stored. # # Bugs/features TODO... # - option/default to use absolute path when going through / # - support -f option to overwrite files # - similar options to overwrite only symlinks or only dead symlinks # - support --target-directory option for xargs compliance # - other gnu options, e.g. --version # - fix error exit codes to be more meaningful or standards-compliant # - indentation # - optimization? It'll be easier to write an algorithm using c. # - option to follow symlinks if possible? (use pwd and realpath) # - allow linking to files that do not exist, or to dead symlinks? # - allow defaulting of destination "." ? # - anything else? more documentation? USAGE="Usage: rln target1 [target2 target3 ...] destination\nFor each target, a relative symlink from destination to target will be placed in destination.\nIf there is _only one_ target, destination may be a new file name." if (( $# < 2 )) || \ [[ "$1" == "--help" ]] || \ [[ "$1" == "-h" ]] || \ [[ "$1" == "-?" ]]; then echo -e "$USAGE" exit 0 fi # The final parameter is assumed to be the destination eval final_dest=\$$# # It ought to be a directory... if [ -d "$final_dest" ]; then dest_dir=`realpath "$final_dest"` # but if it's not a directory, it should not exist... elif [ -e "$final_dest" ]; then echo "Destination already exists, and is not a directory: $final_dest" exit 1 else # we'll need the dest_dir several times... dest_dir=`dirname "$final_dest"` # The parent directory must exist... if [ ! -d "$dest_dir" ]; then echo "Parent directory of destination does not exist: $dest_dir" exit 1 # okay, so the destination will be the name of the symlink file # however, this means that there can be only one target elif (( $# > 2 )); then echo -e "$USAGE" exit 1 # And it looks like we have a valid destination... else dest_dir=`realpath "$dest_dir"` fi fi while (( $# > 1 )); do # grab the first file to be linked... target="$1" # and shift the list; note we stop when $# == 1 ($1 == destination) shift # We'll need to work with the absolute path true_target=`realpath "$target"` if (( $? != 0 )); then echo "error... realpath $target" realpath "$target" exit 1 fi cut_target=`echo "$true_target" | sed 's/^\///'` cut_dest=`echo "$dest_dir/" | sed 's/^\///'` root_target=`echo "$cut_target" | sed -r 's/^([^/]+\/).*/\1/'` root_dest=`echo "$cut_dest" | sed -r 's/^([^/]+\/).*/\1/'` while [[ "$root_target" == "$root_dest" ]]; do len_root=`expr length "$root_dest"` cut_target=`expr substr "$cut_target" $((len_root+1)) 65535` cut_dest=`expr substr "$cut_dest" $((len_root+1)) 65535` root_target=`echo "$cut_target" | sed -r 's/^([^/]+\/).*/\1/'` root_dest=`echo "$cut_dest" | sed -r 's/^([^/]+\/).*/\1/'` done # replace each directory level with a "../" rel_dest=`echo "$cut_dest" | sed -r 's/[^/]+\//..\//g'` # and paste those "../"s in front of the target... rel_target="${rel_dest}${cut_target}" ln -s $rel_target $final_dest done exit 0