4

I am preparing a bash script to validate correctness of symlinks. I want to diagnose when:

  1. @symlink is broken
  2. @symlink points to another @symlink -- (fix it with final target of symlinks chain)
  3. @symlink points to another @symlink, which is broken
  4. @symlinks chain is a cycle

I have big problems with point 2) in diagnosing when symlink points to symlink.

I was trying to use readlink, but it returns final target of symlinks chain instead of pointed symlink name. I tried to run it without -f parameter, but it wasn't help. And then combinations with find gave me poor result...
Can anyone help me with this issue?

Below I pasted my code in the current version.

failed=0

for file in path/*
do
    if [[ -L "$file" ]]
    then
        if [[ ! -a "$file" ]]
        then
            echo "Symlink '$file' is broken -- target object doesn't exists."
            failed=1
        elif [[ -L "$(readlink -f $file)" ]]
        then
            echo "Symlink '$file' points to another symlink: '$(readlink $file)'"
            failed=1
        fi
    fi
done
exit $failed

UPDATE

Testing files structure (where symlinks.sh is discussed bash script):

**hook-tests**

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Hunter_71
  • 649
  • 1
  • 9
  • 16

2 Answers2

1

It's not clear from your problem description how a broken link at the end of a transitive chain should be handled. I will simply report an error and move on.

#!/bin/sh
rc=0
for file in path/*; do
    if [ -L "$file" ]; then
        nfile=$file
        while [ -L "$nfile" ]; do
            nfile=$(cd $(dirname "$nfile"); abspath $(readlink "$nfile"))
        done
        if ! [ -r "$nfile" ]; then
            echo "$0: $file eventually resolves to nonexistent target $nfile" >&2
            rc=1
        else
            # FIXME: maybe avoid doing this needlessly?
            rm "$file"
            ln -s "$nfile" "$file"
        fi
    fi
done
exit "$rc"

Do I understand correctly that you want a symlink to a symlink (etc recursively) to be replaced with a symlink to the final target? This script does that, albeit somewhat excessively in that it will rewrite all symlinks which resolve; optimizing this to avoid doing it needlessly is left as an exercise.

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • When I tried to run your script on my testing files structure (UPDATE section in my question) i get: **symlinks.sh: symlinks/sym_broken eventually resolves to nonexistent target ../noexist (...)** and similar for all 6 symlinks in _symlinks_ directory, when it should appear only for those 2 with "broken" keyword. Have you got any ideas why it doesn't work as expected? PS. of course i changed `path` directory from your code to `symlinks` – Hunter_71 Dec 14 '15 at 12:03
  • And about handling of broken link at the end of chain - your decision to report it as en error is perfect for me :) – Hunter_71 Dec 14 '15 at 12:12
  • See updated answer. The bug was in relative symlinks -- the original code interpreted them as relative to the current directory, while the correct solution is to resolve them relative to the directory of the symlink we are resolving. I also fixed a bug in overwriting a symlink to a directory with an updated link. http://stackoverflow.com/a/3572105/874188 has a portable `abspath` function for OS X if you should need that. – tripleee Dec 14 '15 at 14:17
  • thanks a lot :) Your code and suggestions were very helpful :) I try to publish my extended version of this script when i finish it. It would be nice to read your tips and comments then. best :) – Hunter_71 Dec 15 '15 at 15:04
0

I am not an expert in bash, so it's possible that someone show us much simpler solution, but on this time I share my script (it ensures relative paths for symlinks with relative links):

#!/bin/sh
# The task is to create bash script, which validates symlinks and reports when:
# (1) - symlink is broken
# (2) - symlink target point is broken
# (3) - symlink points to another symlink
# (4) - symlinks chain is a cycle

FAILED=0
FIX_SYMLINKS=false
DIR_UP="../"


function file_under_symlink_absolute_path {
    local file_under_symlink="$(readlink $1)"
    local file_dirname="$(dirname $1)"

    if [[ "$file_under_symlink" == *"$DIR_UP"* ]]; then
        if [[ "$file_under_symlink" == *"$DIR_UP$DIR_UP"* ]]; then
            while [[ "$file_under_symlink" == *"$DIR_UP"* ]]; do
                local file_under_symlink="${file_under_symlink#$DIR_UP}"

                if [[ "$file_dirname" == *"/"* ]]; then
                    local file_dirname="${file_dirname%/*}"
                else
                    local file_dirname=""
                fi
            done

            if [[ "$file_dirname" != "" ]]; then
                local file_dirname="$file_dirname/$file_under_symlink"
            else
                local file_dirname="$file_under_symlink"
            fi
        else
            if [[ "$file_dirname" == *"/"* ]]; then
                local file_dirname="${file_dirname%/*}/${file_under_symlink#$DIR_UP}"
            else
                local file_dirname="${file_under_symlink#$DIR_UP}"
            fi
        fi
    else
        local file_dirname="$file_dirname/$file_under_symlink"
    fi

    echo "$(pwd)/$file_dirname"
}

function file_under_symlink_relative_path {
    local file_dirname="$(dirname $1)"
    local symlink_target="$2"
    local file_under_symlink="$3"

    if [[ "$symlink_target" == *"$file_dirname"* ]]; then
        local prefix2cut="$(pwd)/$file_dirname"
        local target="${symlink_target##$prefix2cut}"

    else
        local symlink_target_dirname="$(dirname $symlink_target)"
        local symlink_target_basename="$(basename $symlink_target)"

        if [[ "$file_under_symlink" == "$symlink_target_dirname"* ]]; then
            local level_diff="${file_under_symlink#$symlink_target_dirname/}"
            local target="$symlink_target_basename"

            while [[ "$level_diff" == *"/"* ]]; do
                local level_diff="${level_diff%/*}"
                local target="$DIR_UP$target"
            done
        else
            if [[ "$file_dirname" == *"/"* ]]; then
                local prefix2cut="$(pwd)/${file_dirname%/*}/"
            else
                local prefix2cut="$(pwd)/"
            fi
            local target="$DIR_UP${symlink_target#$prefix2cut}"
        fi
    fi

    echo "$target"
}

function valid_symlinks {
    local current_dir="$1"

    for file in "$current_dir"/*; do
        if [[ -d "$file" ]]; then
            valid_symlinks "$file"

        elif [[ -L "$file" ]]; then
            local symlink_target=$(readlink -e "$file")
            local file_under_symlink_abs="$(file_under_symlink_absolute_path $file)"

            # reports (1), (2), (4)
            if [[ ! -a "$file" ]]; then
                echo "BROKEN   Symlink '$file' is broken, target object doesn't exists."
                FAILED=1

            # reports (3)
            # it happends when file under symlink is not the symlink target file
            elif [[ "$file_under_symlink_abs" != "$symlink_target" ]]; then
                if $FIX_SYMLINKS && [[ -r "$symlink_target" ]]; then
                    local target="$(file_under_symlink_relative_path $file $symlink_target $file_under_symlink_abs)"

                    echo "Symlink '$file' points to another symlink. It will be replace with direct symlink to target '$target'."
                    ln -fs "$target" "$file"
                else
                    local target="${file_under_symlink_abs#$(pwd)/}"

                    echo "Symlink '$file' points to another symlink '$target'."
                    FAILED=1
                fi
            fi
        fi
    done
}

if [[ -d "$1" ]]; then
    start_point=${1#$(pwd)/}
    start_point=${start_point%/}

    valid_symlinks "$start_point"
else
    echo "ERROR: You have to specify the start location path."
    FAILED=1
fi

exit $FAILED
Hunter_71
  • 649
  • 1
  • 9
  • 16
  • 1
    The repeated `local` declarations are superfluous. You should declare a variable `local` only once, typically at the beginning of the function. – tripleee Dec 16 '15 at 12:31