Say you save the following script as
args:
Code:
#!/bin/bash
for ARG in "$@" ; do
printf 'Argument "%s"\n' "$ARG"
done
What happens when you run
./args /somewhere/*
The shell will first apply
parameter expansion. It sees that the second item is a
glob pattern, and will expand it to the list of all matching files. If you have files
/somewhere/one and
/somewhere/two, then the output will be
Code:
Argument "/somewhere/one"
Argument "/somewhere/two"
In other words, the script does not need to worry about locating the files, as the shell will do it for you.
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
I would approach the problem in a different way. I'd expect my script to take at least two parameters, the first parameter being the directory to create the links in, and all others would be the desired symlink targets,
absolute or relative to current directory. You could then use tab expansion in most shells to find both the link directory, and the symlink target files.
The one issue with that approach is that both are specified as relative to current directory, whereas for the
ln command, the symlink target is always relative to the symlink itself. In other words, we get/know
some-path-to-A and
some-path-to-B, but need to supply
from-A-to-B to
ln -s.
The beginning of the script is simple. If there are not enough parameters, then output usage. (Although I am using Bash, I tend to use POSIX shell idioms. Others will recommend Bash-specific replacements. If you only use Bash for scripting, go with Bash; I'm a crufty curmudgeon.)
Code:
#!/bin/bash
# Make sure locale is POSIX, so we do not choke on non-UTF8 file names in UTF-8 locales.
export LANG=C
export LC_ALL=C
# Use / as the input field separator.
IFS="/"
# Output help if not enough parameters or -h or --help specified.
if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
exec >&2
echo ""
echo "Usage: $0 -h | --help"
echo " $0 LINK-DIRECTORY TARGET(s).."
echo ""
echo "This script will create a symlink to each TARGET in LINK-DIRECTORY."
echo ""
exit 0
fi
Next, we remove the link directory from the parameter list, and shift the rest one place up.
Code:
# Pop the link directory from the parameter list.
LINKDIR="$1"
shift 1
To find where it actually leads, I use a subshell to change to that directory, then run
/bin/pwd to find out its actual path, and capture it to a variable. If it fails -- say, the directory does not exist --,
cd will output an error message, and we can just abort the script.
Code:
# Find out the path to link directory. If it is not a real directory, abort.
LINKPATH="$(cd "$LINKDIR" && /bin/pwd)" || exit $?
Next, we loop over all leftover parameters. This way we only need to worry about the current link within the loop:
Code:
# Loop over all other arguments. (The link directory was shifted out.)
for TARGET in "$@" ; do
# Verify the target exists.
if [ ! -e "$TARGET" ]; then
echo "$TARGET: No such file or directory." >&2
exit 1
fi
Next, we split the target into the file name part (which will be the symlink name, of course), and the actual path. For the path I use the same /bin/pwd trick as earlier, except this time I suppress the error message cd might produce by redirecting standard error to /dev/null:
Code:
# Filename part of target,
TARGETFILE="$(basename "$TARGET")"
# and full path part of target.
if ! TARGETPATH="$(cd "$(dirname "$TARGET")" 2>/dev/null && /bin/pwd)" ; then
echo "$TARGET: Directory does not exist." >&2
exit 1
fi
Next comes the tricky bit. I construct two strings, the first of which describes the directories that need to be ascended from the link directory, and the second describes the path to then descend, to get from link directory to target:
Code:
# The list of directories needed to ascend first from link dir,
uplist="$LINKPATH"
# then to descend down to target dir.
downlist="$TARGETPATH/${TARGETFILE#/}"
Since there is no reason to ascend a directory if you'll immediately descend into it anyway, we can remove the leading common path segments from both:
Code:
# Remove leading common directories from uplist and downlist.
while [ "${uplist%%/*}" = "${downlist%%/*}" ] && [ -n "$uplist" ] && [ -n "$downlist" ]; do
olduplist="$uplist"
uplist="${uplist#*/}"
[ "$olduplist" = "$uplist" ] && uplist=""
olddownlist="$downlist"
downlist="${downlist#*/}"
[ "$olddownlist" = "$downlist" ] && downlist=""
done
If there are no slashes, then
${var#*/} evaluates to
${var} (i.e. no change!) so we need to explicitly check if there were no slashes. It is easiest to do by comparing against the unmodified value.
Next, we can construct the actual path. Start with ./ so that there will always be a trailing slash. We can remove it after the path is constructed:
Code:
# The symlink starts at the link directory.
LINK="."
# The symlink the ascends each directory up from the link directory,
LINK=".$(echo "/$uplist/" | sed -e 's|[^/]\+|..|g; s|//\+|/|g')"
# then descends down into the target directory and item.
LINK="${LINK}$downlist"
# Remove the superfluous "./" at the start of the symlink,
LINK="${LINK#./}"
# as well as any trailing slashes.
LINK="${LINK%/}"
Now
$LINK is the path from the link directory to the target item. So, create the symlink:
Code:
# Create the symlink; abort if failure.
ln -s "$LINK" "$LINKPATH/$TARGETFILE" || exit $?
done
That's it. If you run the script with some test arguments, you'll see that the symlinks are always relative; this is due to the path walking tricky part in the middle. It is also what makes the script useful compared to just using plain
ln -s .
Note that the above script uses POSIX idioms for a reason: it should run using
dash too, not just Bash (by changing only the first line to
#!/bin/dash ). If you always have Bash at your fingerprints, you can clean up the syntax quite a bit using Bash-only features. Like I said, I'm just stuck in my ways. For now.
For debugging, I recommend you modify the second-to-last line to
echo ln -s , and perhaps sprinkle some informative
echo or
printf lines here and there. (
printf is supported by both Bash and POSIX shells; use
echo only for unformatted string output.)
Hope you find this informative,