Kaz Kylheku <
157-07...@kylheku.com>:
> The following is intended to replicate the salient feature of
> the famous "lndir" utility from X11.
>
> lndir fromdir todir creates a mirror of the fromdir directory
> structure rooted at todir, and populates the mirror with symlinks
> to the original files.
>
> The links are relative if the fromdir argument is relative.
>
> lndir is very useful; with lndir you can build code in
> build directory separate from the source tree. (Even code
> which has no build support for this whatsoever.)
>
> The shell dialect is POSIX; hence no "local var=$1" is used.
You could use
lndir() ( )
rather than
lndir() { ;}
to obviate the "local var" command, should it be necessary.
> lndir()
> {
> fromdir=$1
> todir=$2
> abs=${fromdir%${fromdir#/}}
>
> find "$fromdir" \( -type f -o -type d \) | while read frompath ; do
What about inodes in the "$fromdir" tree, that are neither regular
files nor directories, for example, symbolic links?
find ... | while IFS= read -r frompath
...
done
will fail, if there are any newlines in any pathname component.
A way to deal with this for example would be
find ... -exec sh -c -- '
for frompath
do
...
done' sh '{}' +
but that would make counting the depth of the "$todir/$topath" by
> old_IFS=$IFS
> IFS=/
> set -- $todir/$topath
> IFS=$old_IFS
> dots=""
> while [ $# -gt 0 ] ; do
> [ $1 = "." ] || dots="$dots../"
> shift
> done
impossible, because the positional parameters would be already in
use for the pathnames given to the shell. (One could write a
shell function to gather positional parameters in one shell
variable in a way that could be used with "eval" to restore the
positional parameters, but I don't think it's worth while doing
that, see below.)
Also, it might not work with a "$todir" containing any ".." or
symlink pathname components.
To obviate this problem, I would abolish the "todir" parameter,
requesting that the invoker always changes to the "$todir"
directory prior to invoking lndir() and therefore supplies a
(relative or absolute) "fromdir" parameter, that reaches the
source directory from starting at that then current (i.e. intended
"$todir") directory.
Otherwise, lndir() would have to canonicalize the "$todir"
parameter, i.e., expanding symbolic links, resolving ".." and
removing "." path components.
Now, if "$todir" is always ".", the commands
> old_IFS=$IFS
> IFS=/
> set -- $todir/$topath
> IFS=$old_IFS
> dots=""
> while [ $# -gt 0 ] ; do
> [ $1 = "." ] || dots="$dots../"
> shift
> done
could be replaced by
dots="$(
export LC_COLLATE LC_CTYPE
LC_COLLATE=POSIX; LC_CTYPE=POSIX
printf %s "$topath" |
tr -cd -- / |
tr -- / \\n |
sed -e s/\^/../ | tr -- \\n /
)"
or, if one likes to avoid calling programs (i.e. fork() and
execve(), assuming, that "false" is a shell built-in),
path="$topath" && dots= &&
while basename="${path##*/}"
path="${path%"$basename"}"
${path:+:} false
do
path="${path%/}"
dots=../"$dots"
done
> topath=${frompath#$fromdir}
> topath=${topath#/}
> [ -n "$topath" ] || topath="."
> if [ -f "$frompath" ] ; then
> if [ $abs ] ; then
> ln -sf "$frompath" "$todir/$topath"
"ln" might fail, if the explicit end-of-options delimiter "--" is
not used, depending on the values of "$frompath" and
"$todir/$topath".
"ln" won't do, what was intended, when "$todir/$topath" is an
existing directory, but "$frompath" is not.
> else
> old_IFS=$IFS
> IFS=/
> set -- $todir/$topath
> IFS=$old_IFS
> dots=""
> while [ $# -gt 0 ] ; do
> [ $1 = "." ] || dots="$dots../"
> shift
> done
> ln -sf "$dots$frompath" "$todir/$topath"
(Same caveats here.)
> fi
> else
> mkdir -p "$todir/$topath"
(end-of-option delimiter recommended here, too)
> fi
> done
> }
WARNING: The following code has not been tested. There is no
error checking for the case, that
* either the source tree is (a part of) the destination tree (then
the files in the source tree might be replaced by dangling symbolic
links referring to themselves),
* or the destination tree is (a part of) the source tree (then it
might recurse till the file system is exhausted).
* Also, as far as I know, there is no way for a utility called from
"find" via "-exec" to tell "find", that it stop traversing the
file hierarchy and terminate.
To test this function, the "ln" and "mkdir" commands could be
replaced by
( set -- ln -sf -- "${1:?}" "$2"/ ; printf %s\\n "$*" )
( set -- ln -sf -- "$4$1" "$2"/ ; printf %s\\n "$*" )
( set -- mkdir -p -- "$topath" ; printf %s\\n "$*" )
respectively, in order to only print, what would be done
otherwise.
lndir()
{
# Invocation:
# lndir fromdir
# "$1" shall be (a symbolic link to) a directory:
if ! test -d "${1:?}"
then
printf >&2 "%s: Not a directory.\\n" "$1"
return 1
fi
# FIXME: What about symbolic links in the
# "$1" tree?
set -- "${1%/}"/
find "$1" \( -type f -o -type d \) \
-exec sh -c -- '
set -e
fromdir="${1:?}"
shift
if test " $1" = " $fromdir"
then
# This is the already existing root of the tree.
# Nothing to do.
shift
fi
abs="${fromdir%"${fromdir#/}"}"
if ${abs:+:} false
then
# "$fromdir" is an absolute pathname.
symlink()
{
# Invocation:
# symlink frompath tosubdir
ln -sf -- "${1:?}" "$2"/
}
else
# "$fromdir" is a relative pathname starting at the
# current directory.
symlink()
{
# Invocation:
# symlink frompath tosubdir
# (ab-)use the positional parameters for local
# named parameters:
set -- "$1" "$2" "${2#.}" ""
# "$@" = frompath tosubdir tosubdirrest dots
while
${3:+:} false
do
set -- "$1" "$2" "${3%/*}" "$4"../
done
ln -sf -- "$4$1" "$2"/
}
fi
# Let "$fromdir" have a trailing slash:
fromdir="${fromdir%/}/"
for frompath
do
topath="${frompath#"$fromdir"}"
# "$topath" is not empty
if test -d "$frompath"
then
# "$frompath" is (a symbolic link to) a directory.
# Treat symbolic links to directories like directories:
mkdir -p -- "$topath"
else
# "$frompath" is not (a symbolic link to) a directory.
# Symlink it:
tosubdir=./"$topath"
symlink "$frompath" "${tosubdir%/*}"
fi
done' sh "$1" '{}' +
}