Convert absolute to relative symbolic link - bash

I rsync the directory "Promotion" containing absolute symbolic links between two machines with different directory structures. Therefore absolute symbolic links don't work on both machines. To make them work, I would like to convert them to relative links.
The directory structure is
Machine 1: /home/user/Privat/Uni Kram/Promotion/
Machine 2: /homes/user/Promotion/
Here are two example symlinks:
4821 1 lrwxrwxrwx 1 manu users 105 Nov 17 2014 ./Planung\ nach\ Priorit\303\244ten.ods -> /home/manu/Dokumente/Privat/Uni\ Kram/Promotion/Pl\303\244ne\ und\ Ideen/Pl\303\244ne/Planung\ nach\ Priorit\303\244ten.ods
37675 1 lrwxrwxrwx 1 manu users 102 Aug 3 2015 ./Kurs/Lab\ Course\ Somewhere -> /home/manu/Dokumente/Privat/Uni\ Kram/Promotion/Workshops\ &\ Fortbildungen/Kurs\ Lab\ Course\ Somewhere
My non-working try is (based on example this):
find * -type l -print | while read l; do
ln -srf $(cut -c 24- < $(readlink $l)) $l;
done

On machine that contains right sym-linked files, run this:
find . -type l -exec ln -sfr {} . \;
to only change the symlinks in the current directory and not traverse all subdirectories use the -maxdepth 1 flag as the above command will move symlinks in subdirectories into the current directory.
Also the native BSD ln command provided by macos does not have a -r flag, but the ln command provided by GNU core utils can be used instead, and if installed via homebrew prepend g to ln ie. gln -sfr

Here is a solution for doing this using python3.
from pathlib import Path
d = Path("/home/user/Privat/Uni Kram/Promotion/")
all_symlinks = [p for p in d.rglob("*") if p.is_symlink()]
def make_symlink_relative(p):
assert p.is_symlink()
relative_target = p.resolve(strict=True).relative_to(p.absolute().parent)
p.unlink()
# this while-loop protects against race conditions
while True:
try:
p.symlink_to(relative_target)
break
except FileExistsError:
pass
for symlink in all_symlinks:
make_symlink_relative(symlink)

Following may work.
Note that
links are be backuped, if not wanted change mv .. by rm
find argument must be absolute otherwise function will reject links
Example
find /absolute/path/to/root -type l -exec bash -c $'
abs_to_relative() {
l=$1
t=$(readlink "$1")
[[ $l = /* ]] && [[ $t = /* ]] && [[ $l != "$t" ]] || return
set -f
IFS=/
link=($l)
target=($t)
IFS=$\' \\t\\n\'
set +f
i=0 f=\'\' res=\'\'
while [[ ${link[i]} = "${target[i]}" ]]; do ((i+=1)); done
link=("${link[#]:i}")
target=("${target[#]:i}")
for ((i=${#link[#]};i>1;i-=1)); do res+=../; done
res=${res:-./}
for f in "${target[#]}"; do res+=$f/; done
res=${res%/}
# rm "$1"
mv "$1" "$1.bak"
ln -s "$res" "$1"
}
for link; do abs_to_relative "$link"; done
' links {} +
Test that was done
mkdir -p /tmp/testlink/home{,s}/user/abc
touch /tmp/testlink/home/user/{file0,abc/file1}.txt
ln -s /tmp/testlink/home/user/abc/file1.txt /tmp/testlink/home/user/link1.txt
ln -s /tmp/testlink/home/user/file0.txt /tmp/testlink/home/user/abc/link0.txt
ln -s /tmp/testlink/home/user/ /tmp/testlink/home/user/abc/linkdir
# ... command
rm -rf /tmp/testlink

Thank you all for your help. After some trying I came up with a solution based on your comments and code.
Here is what solved my problem:
#!/bin/bash
# changes all symbolic links to relative ones recursive from the current directory
find * -type l -print | while read l; do
cp -a "$l" "$l".bak
linkname="$l"
linktarget=$(readlink "$l")
echo "orig linktarget"
echo $linktarget
temp_var="${linktarget#/home/user/Privat/Uni Kram/Promotion/}"
echo "changed linktarget"
echo $temp_var;
ln -sfr "$temp_var" "$l"
echo "new linktarget in symlink"
readlink "$l";
done

Here is a pair of functions to convert in both directions. I now have these in my ~/bin to use more generally. The links do not have to be located in the present directory. Just use as:
lnsrelative <link> <link> <link> ...
lnsabsolute <link> <link> <link> ...
#!/bin/bash
# changes absolute symbolic links to relative
# will work for links & filenames with spaces
for l in "$#"; do
[[ ! -L "$l" ]] && echo "Not a link: $l" && exit 1
done
for l in "$#"; do
# Use -b here to get a backup. Unnecessary because reversible.
ln -sfr "$(readlink "$l")" "$l"
done
and
#!/bin/bash
# changes relative symbolic links to absolute
# will work for links & filenames with spaces
for l in "$#"; do
[[ ! -L "$l" ]] && echo "Not a link: $l" && exit 1
done
for l in "$#"; do
# Use -b here to get a backup. Unnecessary because reversible.
ln -sf "$(realpath "$(readlink "$l")")" "$l"
done
For completeness, this will change symbolic links to hard links, used as:
lnstohard <link> <link> <link>
#!/bin/bash
# changes symbolic links to hard links
# This will work for filenames with spaces,
# but only for regular files in the same filesystem as the link.
# Thorough discussion of correct way to confirm devices are the same:
# https://unix.stackexchange.com/questions/120810/check-if-2-directories-are-hosted-on-the-same-partition-on-linux
for l in "$#"; do
[[ ! -L "$l" ]] && echo "Not a symbolic link: $l" && exit 1
rl="$(readlink "$l")"
rld="$(dirname "$l")"
[[ ! -e "$rl" || -d "$rl" || "$(df --output=target "$rld")" != "$(df --output=target "$rl")" ]] && \
echo "Target \"$rl\" must exist on same filesystem as link \"$l\", and may not be a directory" && \
exit 1
done
for l in "$#"; do
# Using -b here to get a backup, because it's not easy to revers a soft->hard link conversion
ln -fb "$(readlink "$l")" "$l"
done

While #Diagon's answer is more complete, here's a quick and dirty one-liner that works most of the time but will probably die on paths with spaces:
find . -type l -exec bash -c 'ln -sfr {} $(dirname {})/' \;

Related

Bash complete function - Separating completion parts with character other than space

I've written a bash completion script to essentially do file/directory completion, but using . as the separator instead of /. However, it's not behaving as I expect it to.
Before I dive further, does anyone know of any options for this, or something that's already been written that can do this? The motivation for this is to enable completion when calling python with the -m flag. It seems crazy that this doesn't exist yet, but I was unable to find anything relevant.
My issue is that bash doesn't recognize . as a separator for completion options, and won't show the next options until I add an additional space to the end of the current command.
Here's a few concrete examples, given this directory structure.
/module
/script1.py
/script2.py
For instance, when I use the ls command, it works like this
$ ls mo<TAB>
$ ls module/<TAB><TAB>
script1.py script2.py
However, with my function, it's working like this:
$ python -m mod<TAB>
$ python -m module.<TAB><TAB>
module.
So instead of showing the next entries, it just shows the finished string again. However, if I add a space, it then works, but I don't want it to include the space:
$ python -m mod<TAB>
$ python -m module. <TAB><TAB> # (note the space here after the dot)
script1 script2 # (Note, I'm intentionally removing the file extension here).
I'd like the completion to act just like the bottom example, except not be forced to include the space to go to the next set of options
I've got about 50 tabs open and I've tried a bunch of recommendations, but nothing seems to be able to solve this how I'd like. There are a few other caveats here that would take a lot of time to go through, so I'm happy to expand on any other points if I've skipped something important. I've attached my code below, any help would be greatly appreciated. Thanks!
#!/bin/bash
_python_target() {
local cur opts cur_path
# Retrieving the current typed argument
cur="${COMP_WORDS[COMP_CWORD]}"
# Preparing an array to store available list for completions
# COMREPLY will be checked to suggest the list
COMPREPLY=()
# Here, we'll only handle the case of "-m"
# Hence, the classic autocompletion is disabled
# (ie COMREPLY stays an empty array)
if [[ "${COMP_WORDS[1]}" != "-m" ]]
then
return 0
fi
# add each path component to the current path to check for additional files
cur_path=""
for word in ${COMP_WORDS[#]:2:COMP_CWORD-2}; do
path_component=$(echo ${word} | sed 's/\./\//g')
cur_path="${cur_path}${path_component}"
done
cur_path="./${cur_path}"
if [[ ! -f "$cur_path" && ! -d "$cur_path" ]]; then
return 0
fi
# this is not very pretty, but it works. Open to comments on this too
file_opts="$(find ${cur_path} -name "*.py" -type f -maxdepth 1 -print0 | xargs -0 basename -a | sed 's/\.[^.]*$//')"
dir_opts="$(find ${cur_path} ! -path ${cur_path} -type d -maxdepth 1 -print0 | xargs -0 basename -a | xargs -I {} echo {}.)"
opts="${file_opts} ${dir_opts}"
# We store the whole list by invoking "compgen" and filling
# COMREPLY with its output content.
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
[[ $COMPREPLY == *\. ]] && compopt -o nospace
}
complete -F _python_target python
Here's a draft example:
_python_target()
{
local cmd=$1 cur=$2 pre=$3
if [[ $pre != -m ]]; then
return
fi
local cur_slash=${cur//./\/}
local i arr arr2
arr=( $( compgen -f "$cur_slash" ) )
arr2=()
for i in "${arr[#]}"; do
if [[ -d $i ]]; then
arr2+=( "$i/" )
elif [[ $i == *.py ]]; then
arr2+=( "${i%.py}" )
fi
done
arr2=( "${arr2[#]//\//.}" )
COMPREPLY=( $( compgen -W "${arr2[*]}" -- "$cur" ) )
}
complete -o nospace -F _python_target python
Try with the python-2.7.18 source code directory:

How do I rename and overwrite part of file name in Bash at the same time?

I have multiple JS files in folder, for example, foo.js, this_is_foo.js, foo_is_function.js.
I want to append a number which is passed as parameter in front of extension ".js", such as foo.1.js, this_is_foo.1.js, foo_is_function.1.js
I write a script to append a number successfully the first time, but if I run the script twice, it does not overwrite the first number but append right after that.
Actual result: foo.js --> foo.1.js (1st run) --> foo.1.2.js (2nd run).
Expected result: foo.js --> foo.1.js (1st run) --> foo.2.js (2nd run).
This is my script:
#!/bin/sh
param=$1
for file in *.js; do
ext="${file##*.}";
filename="${file%.*}";
mv "$file" "${filename}.${param}.${ext}";
done
How can I do that? I want to write pure bash script, not to use any tools.
Before doing the rename you can check if what is after the last dot in filename (${filename%.*}) is numeric. If so switch that with param instead of appending a new param
Since you are writing that you want to use pure bash I assume that it is ok to change the shebang to #!/bin/bash:
#!/bin/bash
param=$1
for file in *.js; do
ext="${file##*.}";
filename="${file%.*}";
# Check if what is after the last dot in filename is numeric
# Then assume that it should be switched to param
if [[ ${filename##*.} =~ ^[0-9]+$ ]]; then
mv "$file" "${filename%.*}.${param}.${ext}"
## Else add param
else
mv "$file" "${filename}.${param}.${ext}"
fi
done
Testrun
$> touch a.js && find -type f -name *.js && \
./test.sh 1 && find -type f -name *.js && \
./test.sh 2 && find -type f -name *.js
./a.js
./a.1.js
./a.2.js
You can use extended globbing to match an optional . (the one before the number) and any amount of digits before the .js suffix.
I also added the . to the $param variable if the variable is non-empty. This way you can call the script without parameter and .<number> is removed instead of added/changed.
#!/bin/bash
shopt -s extglob # enable extended globbing
param=$1
if [ -n "$param" ]; then
param=.${param} # add prefix `.`
fi
for file in *.js; do
newfile=${file%%?(.)*([0-9]).js}${param}.js
if [ "$file" = "$newfile" ]; then
echo "skipping \"${file}\", no need to rename"
elif [ -f "$newfile" ]; then
echo "skipping \"$file\", file \"$newfile\" already exists"
else
mv "$file" "$newfile"
fi
done
Usage:
./script.sh 1 # change suffix to `.1.js`
./script.sh 2 # change suffix to `.2.js`
./script.sh # change suffix to `.js`
Posix compliant solution, which allows you to keep using /bin/sh
#!/bin/sh
param=$1
for file in *.js; do
ext="${file##*.}";
filename="${file%.*}";
[ -z "${filename##*.##*[!0-9]*}" ] && mv "$file" "${filename}.${param}.${ext}" || mv "$file" "${filename%.*}.${param}.${ext}"
done
Testrun
$> touch a.js && find -type f -name *.js && \
./test.sh 1 && find -type f -name *.js && \
./test.sh 2 && find -type f -name *.js
./a.js
./a.1.js
./a.2.js
This might do what you want.
#!/usr/bin/env bash
counter=1
for file in *.js; do
if [[ $file =~ (^[^\.]+)(\.js)$ ]]; then
mv -v "$file" "${BASH_REMATCH[1]}.$counter${BASH_REMATCH[2]}"
elif [[ $file =~ (^[^\.]+)\.([[:digit:]]+)(\.js)$ ]]; then
first=${BASH_REMATCH[1]}
counter=${BASH_REMATCH[2]}
extension=${BASH_REMATCH[3]}
((counter++))
mv -v "$file" "$first.$counter$extension"
fi
done
In action.
mkdir -p /tmp/123 && cd /tmp/123
touch foo.js
touch this_is_foo.js
touch this_is_foucntion.js
First run
./myscript
Output
renamed 'foo.js' -> 'foo.1.js'
renamed 'this_is_foo.js' -> 'this_is_foo.1.js'
renamed 'this_is_foucntion.js' -> 'this_is_foucntion.1.js'
Second run.
renamed 'foo.1.js' -> 'foo.2.js'
renamed 'this_is_foo.1.js' -> 'this_is_foo.2.js'
renamed 'this_is_foucntion.1.js' -> 'this_is_foucntion.2.js'
Third run.
renamed 'foo.2.js' -> 'foo.3.js'
renamed 'this_is_foo.2.js' -> 'this_is_foo.3.js'
renamed 'this_is_foucntion.2.js' -> 'this_is_foucntion.3.js'
Using a for loop
for i in {1..5}; do ./script.sh ; done
Output
renamed 'foo.js' -> 'foo.1.js'
renamed 'this_is_foo.js' -> 'this_is_foo.1.js'
renamed 'this_is_foucntion.js' -> 'this_is_foucntion.1.js'
renamed 'foo.1.js' -> 'foo.2.js'
renamed 'this_is_foo.1.js' -> 'this_is_foo.2.js'
renamed 'this_is_foucntion.1.js' -> 'this_is_foucntion.2.js'
renamed 'foo.2.js' -> 'foo.3.js'
renamed 'this_is_foo.2.js' -> 'this_is_foo.3.js'
renamed 'this_is_foucntion.2.js' -> 'this_is_foucntion.3.js'
renamed 'foo.3.js' -> 'foo.4.js'
renamed 'this_is_foo.3.js' -> 'this_is_foo.4.js'
renamed 'this_is_foucntion.3.js' -> 'this_is_foucntion.4.js'
renamed 'foo.4.js' -> 'foo.5.js'
renamed 'this_is_foo.4.js' -> 'this_is_foo.5.js'
renamed 'this_is_foucntion.4.js' -> 'this_is_foucntion.5.js'

Move directory from queue directory Bash script

I'm trying to implement directory Queue.
I have the following directories:
Q_Dir
folder1
subfolder1
...
subfolderN
files1....filesN+X
....
Target_Dir
folder1
subfolder1
....
subfolderN
files1...filesN
....
I want to move maximum X files from Q_Dir to Target_Dir.
Pseudo code:
While True:
totalFiles = Count of total files in Target_Dir
If totalFiles < X then:
Move X-totalFiles files From Q_Dir to Target_Dir
Else
Sleep 5 seconds
I looking for the best solution in Linux bash script to do it
Any suggestions?
Consider the following implementation of the pseudo-code. It is a one-to-one implementation. Possible to improve, if more details will be available.
S=Q_Dir
T=Target_Dir
X=6
mkdir -p $T
while true ; do
t_count=$(find $T -type f | wc -l)
if [[ "$t_count" -lt "$X" ]] ; then
readarray -t -n "$((X-t_count))" files <<< "$(cd $S && find . -type f)"
echo "F=${#files[#]}"
for f in "${files[#]}" ; do
d=${f%/*}
mkdir -p $T/$d
mv $S/$f $T/$d/
done
sleep 3
else
sleep 5
fi
done
Note that the code does not provide atomic update - if multiple instances of the script will be executing against the same source or target folder.

How to split the file path to extract the various subfolders into variables? (Ubuntu Bash)

I need help with Ubuntu Precise bash script.
I have several tiff files in various folders
masterFOlder--masterSub1 --masterSub1-1 --file1.tif
|--masterSub1-2 --masterSub1-2-1 --file2.tif
|
|--masterSub2 --masterSub1-2 .....
I need to run an Imagemagick command and save them to new folder "converted" while retaining the sub folder tree i.e. the new tree will be
converted --masterSub1 --masterSub1-1 --file1.png
|--masterSub1-2 --masterSub1-2-1 --file2.png
|
|--masterSub2 --masterSub1-2 .....
How do i split the filepath into folders, replace the first folder (masterFOlder to converted) and recreate a new file path?
Thanks to everyone reading this.
This script should work.
#!/bin/bash
shopt -s extglob && [[ $# -eq 2 && -n $1 && -n $2 ]] || exit
MASTERFOLDER=${1%%+(/)}/
CONVERTFOLDER=$2
OFFSET=${#MASTERFOLDER}
while read -r FILE; do
CPATH=${FILE:OFFSET}
CPATH=${CONVERTFOLDER}/${CPATH%.???}.png
CDIR=${CPATH%/*}
echo "Converting $FILE to $CPATH."
[[ -d $CDIR ]] || mkdir -p "$CDIR" && echo convert "$FILE" "$CPATH" || echo "Conversion failed."
done < <(exec find "${MASTERFOLDER}" -mindepth 1 -type f -iname '*.tif')
Just replace echo convert "$FILE" "$CPATH" with the actual command you use and run bash script.sh masterfolder convertedfolder

Bash script absolute path with OS X

I am trying to obtain the absolute path to the currently running script on OS X.
I saw many replies going for readlink -f $0. However since OS X's readlink is the same as BSD's, it just doesn't work (it works with GNU's version).
Is there an out-of-the-box solution to this?
These three simple steps are going to solve this and many other OS X issues:
Install Homebrew
brew install coreutils
grealpath .
(3) may be changed to just realpath, see (2) output
There's a realpath() C function that'll do the job, but I'm not seeing anything available on the command-line. Here's a quick and dirty replacement:
#!/bin/bash
realpath() {
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}
realpath "$0"
This prints the path verbatim if it begins with a /. If not it must be a relative path, so it prepends $PWD to the front. The #./ part strips off ./ from the front of $1.
I found the answer a bit wanting for a few reasons:
in particular, they don't resolve multiple levels of symbolic links, and they are extremely "Bash-y".
While the original question does explicitly ask for a "Bash script", it also makes mention of Mac OS X's BSD-like, non-GNU readlink.
So here's an attempt at some reasonable portability (I've checked it with bash as 'sh' and dash), resolving an arbitrary number of symbolic links; and it should also work with whitespace in the path(s).
This answer was previously edited, re-adding the local bashism. The point of this answer is a portable, POSIX solution. I have edited it to address variable scoping by changing it to a subshell function, rather than an inline one. Please do not edit.
#!/bin/sh
realpath() (
OURPWD=$PWD
cd "$(dirname "$1")"
LINK=$(readlink "$(basename "$1")")
while [ "$LINK" ]; do
cd "$(dirname "$LINK")"
LINK=$(readlink "$(basename "$1")")
done
REALPATH="$PWD/$(basename "$1")"
cd "$OURPWD"
echo "$REALPATH"
)
realpath "$#"
Hope that can be of some use to someone.
A more command-line-friendly variant of the Python solution:
python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' ./my/path
Since there is a realpath as others have pointed out:
// realpath.c
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char* argv[])
{
if (argc > 1) {
for (int argIter = 1; argIter < argc; ++argIter) {
char *resolved_path_buffer = NULL;
char *result = realpath(argv[argIter], resolved_path_buffer);
puts(result);
if (result != NULL) {
free(result);
}
}
}
return 0;
}
Makefile:
#Makefile
OBJ = realpath.o
%.o: %.c
$(CC) -c -o $# $< $(CFLAGS)
realpath: $(OBJ)
gcc -o $# $^ $(CFLAGS)
Then compile with make and put in a soft link with:
ln -s $(pwd)/realpath /usr/local/bin/realpath
abs_path () {
echo "$(cd $(dirname "$1");pwd)/$(basename "$1")"
}
dirname will give the directory name of /path/to/file, i.e. /path/to.
cd /path/to; pwd ensures that the path is absolute.
basename will give just the filename in /path/to/file, i.e.file.
I checked every answered, but missed the best one (IMHO) by Jason S Jul 14 '16 at 3:12, left the comment field.
So here it is, in case someone like me having the tendency to check answered and don't have time to go through every single comments:
$( cd "$(dirname "$0")" ; pwd -P )
Help:
NAME
pwd -- return working directory name
SYNOPSIS
pwd [-L | -P]
DESCRIPTION
The pwd utility writes the absolute pathname of the current working
directory to the standard output.
Some shells may provide a builtin pwd command which is similar or identi-
cal to this utility. Consult the builtin(1) manual page.
The options are as follows:
-L Display the logical current working directory.
-P Display the physical current working directory (all symbolic
links resolved).
I was looking for a solution for use in a system provision script, i.e., run before Homebrew is even installed. Lacking a proper solution I'd just offload the task to a cross-platform language, e.g., Perl:
script_abspath=$(perl -e 'use Cwd "abs_path"; print abs_path(#ARGV[0])' -- "$0")
More often what we actually want is the containing directory:
here=$(perl -e 'use File::Basename; use Cwd "abs_path"; print dirname(abs_path(#ARGV[0]));' -- "$0")
Use Python to get it:
#!/usr/bin/env python
import os
import sys
print(os.path.realpath(sys.argv[1]))
realpath for Mac OS X
realpath() {
path=`eval echo "$1"`
folder=$(dirname "$path")
echo $(cd "$folder"; pwd)/$(basename "$path");
}
Example with related path:
realpath "../scripts/test.sh"
Example with home folder
realpath "~/Test/../Test/scripts/test.sh"
So as you can see above, I took a shot at this about 6 months ago. I totally
forgot about it until I found myself in need of a similar thing again. I was
completely shocked to see just how rudimentary it was; I've been teaching
myself to code pretty intensively for about a year now, but I often feel like
maybe I haven't learned anything at all when things are at their worst.
I would remove the 'solution' above, but I really like it sort of being a record of
of how much I really have learnt over the past few months.
But I digress. I sat down and worked it all out last night. The explanation in
the comments should be sufficient. If you want to track the copy I'm continuing
to work on, you can follow this gist. This probably does what you need.
#!/bin/sh # dash bash ksh # !zsh (issues). G. Nixon, 12/2013. Public domain.
## 'linkread' or 'fullpath' or (you choose) is a little tool to recursively
## dereference symbolic links (ala 'readlink') until the originating file
## is found. This is effectively the same function provided in stdlib.h as
## 'realpath' and on the command line in GNU 'readlink -f'.
## Neither of these tools, however, are particularly accessible on the many
## systems that do not have the GNU implementation of readlink, nor ship
## with a system compiler (not to mention the requisite knowledge of C).
## This script is written with portability and (to the extent possible, speed)
## in mind, hence the use of printf for echo and case statements where they
## can be substituded for test, though I've had to scale back a bit on that.
## It is (to the best of my knowledge) written in standard POSIX shell, and
## has been tested with bash-as-bin-sh, dash, and ksh93. zsh seems to have
## issues with it, though I'm not sure why; so probably best to avoid for now.
## Particularly useful (in fact, the reason I wrote this) is the fact that
## it can be used within a shell script to find the path of the script itself.
## (I am sure the shell knows this already; but most likely for the sake of
## security it is not made readily available. The implementation of "$0"
## specificies that the $0 must be the location of **last** symbolic link in
## a chain, or wherever it resides in the path.) This can be used for some
## ...interesting things, like self-duplicating and self-modifiying scripts.
## Currently supported are three errors: whether the file specified exists
## (ala ENOENT), whether its target exists/is accessible; and the special
## case of when a sybolic link references itself "foo -> foo": a common error
## for beginners, since 'ln' does not produce an error if the order of link
## and target are reversed on the command line. (See POSIX signal ELOOP.)
## It would probably be rather simple to write to use this as a basis for
## a pure shell implementation of the 'symlinks' util included with Linux.
## As an aside, the amount of code below **completely** belies the amount
## effort it took to get this right -- but I guess that's coding for you.
##===-------------------------------------------------------------------===##
for argv; do :; done # Last parameter on command line, for options parsing.
## Error messages. Use functions so that we can sub in when the error occurs.
recurses(){ printf "Self-referential:\n\t$argv ->\n\t$argv\n" ;}
dangling(){ printf "Broken symlink:\n\t$argv ->\n\t"$(readlink "$argv")"\n" ;}
errnoent(){ printf "No such file: "$#"\n" ;} # Borrow a horrible signal name.
# Probably best not to install as 'pathfull', if you can avoid it.
pathfull(){ cd "$(dirname "$#")"; link="$(readlink "$(basename "$#")")"
## 'test and 'ls' report different status for bad symlinks, so we use this.
if [ ! -e "$#" ]; then if $(ls -d "$#" 2>/dev/null) 2>/dev/null; then
errnoent 1>&2; exit 1; elif [ ! -e "$#" -a "$link" = "$#" ]; then
recurses 1>&2; exit 1; elif [ ! -e "$#" ] && [ ! -z "$link" ]; then
dangling 1>&2; exit 1; fi
fi
## Not a link, but there might be one in the path, so 'cd' and 'pwd'.
if [ -z "$link" ]; then if [ "$(dirname "$#" | cut -c1)" = '/' ]; then
printf "$#\n"; exit 0; else printf "$(pwd)/$(basename "$#")\n"; fi; exit 0
fi
## Walk the symlinks back to the origin. Calls itself recursivly as needed.
while [ "$link" ]; do
cd "$(dirname "$link")"; newlink="$(readlink "$(basename "$link")")"
case "$newlink" in
"$link") dangling 1>&2 && exit 1 ;;
'') printf "$(pwd)/$(basename "$link")\n"; exit 0 ;;
*) link="$newlink" && pathfull "$link" ;;
esac
done
printf "$(pwd)/$(basename "$newlink")\n"
}
## Demo. Install somewhere deep in the filesystem, then symlink somewhere
## else, symlink again (maybe with a different name) elsewhere, and link
## back into the directory you started in (or something.) The absolute path
## of the script will always be reported in the usage, along with "$0".
if [ -z "$argv" ]; then scriptname="$(pathfull "$0")"
# Yay ANSI l33t codes! Fancy.
printf "\n\033[3mfrom/as: \033[4m$0\033[0m\n\n\033[1mUSAGE:\033[0m "
printf "\033[4m$scriptname\033[24m [ link | file | dir ]\n\n "
printf "Recursive readlink for the authoritative file, symlink after "
printf "symlink.\n\n\n \033[4m$scriptname\033[24m\n\n "
printf " From within an invocation of a script, locate the script's "
printf "own file\n (no matter where it has been linked or "
printf "from where it is being called).\n\n"
else pathfull "$#"
fi
On macOS, the only solution that I've found to this that reliably handles symlinks is by using realpath. Since this requires brew install coreutils, I just automated that step. My implementation looks like this:
#!/usr/bin/env bash
set -e
if ! which realpath >&/dev/null; then
if ! which brew >&/dev/null; then
msg="ERROR: This script requires brew. See https://brew.sh for installation instructions."
echo "$(tput setaf 1)$msg$(tput sgr0)" >&2
exit 1
fi
echo "Installing coreutils/realpath"
brew install coreutils >&/dev/null
fi
thisDir=$( dirname "`realpath "$0"`" )
echo "This script is run from \"$thisDir\""
This errors if they don't have brew installed, but you could alternatively just install that too. I just didn't feel comfortable automating something that curls arbitrary ruby code from the net.
Note that this an automated variation on Oleg Mikheev's answer.
One important test
One good test of any of these solutions is:
put the code in a script file somewhere
in another directory, symlink (ln -s) to that file
run the script from that symlink
Does the solution dereference the symlink, and give you the original directory? If so, it works.
This seems to work for OSX, doesnt require any binaries, and was pulled from here
function normpath() {
# Remove all /./ sequences.
local path=${1//\/.\//\/}
# Remove dir/.. sequences.
while [[ $path =~ ([^/][^/]*/\.\./) ]]; do
path=${path/${BASH_REMATCH[0]}/}
done
echo $path
}
I like this:
#!/usr/bin/env bash
function realpath() {
local _X="$PWD"
local _LNK=$1
cd "$(dirname "$_LNK")"
if [ -h "$_LNK" ]; then
_LNK="$(readlink "$_LNK")"
cd "$(dirname "$_LNK")"
fi
echo "$PWD/$(basename "$_LNK")"
cd "$_X"
}
I needed a realpath replacement on OS X, one that operates correctly on paths with symlinks and parent references just like readlink -f would. This includes resolving symlinks in the path before resolving parent references; e.g. if you have installed the homebrew coreutils bottle, then run:
$ ln -s /var/log/cups /tmp/linkeddir # symlink to another directory
$ greadlink -f /tmp/linkeddir/.. # canonical path of the link parent
/private/var/log
Note that readlink -f has resolved /tmp/linkeddir before resolving the .. parent dir reference. Of course, there is no readlink -f on Mac either.
So as part of the a bash implementation for realpath I re-implemented what a GNUlib canonicalize_filename_mode(path, CAN_ALL_BUT_LAST) function call does, in Bash 3.2; this is also the function call that GNU readlink -f makes:
# shellcheck shell=bash
set -euo pipefail
_contains() {
# return true if first argument is present in the other arguments
local elem value
value="$1"
shift
for elem in "$#"; do
if [[ $elem == "$value" ]]; then
return 0
fi
done
return 1
}
_canonicalize_filename_mode() {
# resolve any symlink targets, GNU readlink -f style
# where every path component except the last should exist and is
# resolved if it is a symlink. This is essentially a re-implementation
# of canonicalize_filename_mode(path, CAN_ALL_BUT_LAST).
# takes the path to canonicalize as first argument
local path result component seen
seen=()
path="$1"
result="/"
if [[ $path != /* ]]; then # add in current working dir if relative
result="$PWD"
fi
while [[ -n $path ]]; do
component="${path%%/*}"
case "$component" in
'') # empty because it started with /
path="${path:1}" ;;
.) # ./ current directory, do nothing
path="${path:1}" ;;
..) # ../ parent directory
if [[ $result != "/" ]]; then # not at the root?
result="${result%/*}" # then remove one element from the path
fi
path="${path:2}" ;;
*)
# add this component to the result, remove from path
if [[ $result != */ ]]; then
result="$result/"
fi
result="$result$component"
path="${path:${#component}}"
# element must exist, unless this is the final component
if [[ $path =~ [^/] && ! -e $result ]]; then
echo "$1: No such file or directory" >&2
return 1
fi
# if the result is a link, prefix it to the path, to continue resolving
if [[ -L $result ]]; then
if _contains "$result" "${seen[#]+"${seen[#]}"}"; then
# we've seen this link before, abort
echo "$1: Too many levels of symbolic links" >&2
return 1
fi
seen+=("$result")
path="$(readlink "$result")$path"
if [[ $path = /* ]]; then
# if the link is absolute, restart the result from /
result="/"
elif [[ $result != "/" ]]; then
# otherwise remove the basename of the link from the result
result="${result%/*}"
fi
elif [[ $path =~ [^/] && ! -d $result ]]; then
# otherwise all but the last element must be a dir
echo "$1: Not a directory" >&2
return 1
fi
;;
esac
done
echo "$result"
}
It includes circular symlink detection, exiting if the same (intermediary) path is seen twice.
If all you need is readlink -f, then you can use the above as:
readlink() {
if [[ $1 != -f ]]; then # poor-man's option parsing
# delegate to the standard readlink command
command readlink "$#"
return
fi
local path result seenerr
shift
seenerr=
for path in "$#"; do
# by default readlink suppresses error messages
if ! result=$(_canonicalize_filename_mode "$path" 2>/dev/null); then
seenerr=1
continue
fi
echo "$result"
done
if [[ $seenerr ]]; then
return 1;
fi
}
For realpath, I also needed --relative-to and --relative-base support, which give you relative paths after canonicalizing:
_realpath() {
# GNU realpath replacement for bash 3.2 (OS X)
# accepts --relative-to= and --relative-base options
# and produces canonical (relative or absolute) paths for each
# argument on stdout, errors on stderr, and returns 0 on success
# and 1 if at least 1 path triggered an error.
local relative_to relative_base seenerr path
relative_to=
relative_base=
seenerr=
while [[ $# -gt 0 ]]; do
case $1 in
"--relative-to="*)
relative_to=$(_canonicalize_filename_mode "${1#*=}")
shift 1;;
"--relative-base="*)
relative_base=$(_canonicalize_filename_mode "${1#*=}")
shift 1;;
*)
break;;
esac
done
if [[
-n $relative_to
&& -n $relative_base
&& ${relative_to#${relative_base}/} == "$relative_to"
]]; then
# relative_to is not a subdir of relative_base -> ignore both
relative_to=
relative_base=
elif [[ -z $relative_to && -n $relative_base ]]; then
# if relative_to has not been set but relative_base has, then
# set relative_to from relative_base, simplifies logic later on
relative_to="$relative_base"
fi
for path in "$#"; do
if ! real=$(_canonicalize_filename_mode "$path"); then
seenerr=1
continue
fi
# make path relative if so required
if [[
-n $relative_to
&& ( # path must not be outside relative_base to be made relative
-z $relative_base || ${real#${relative_base}/} != "$real"
)
]]; then
local common_part parentrefs
common_part="$relative_to"
parentrefs=
while [[ ${real#${common_part}/} == "$real" ]]; do
common_part="$(dirname "$common_part")"
parentrefs="..${parentrefs:+/$parentrefs}"
done
if [[ $common_part != "/" ]]; then
real="${parentrefs:+${parentrefs}/}${real#${common_part}/}"
fi
fi
echo "$real"
done
if [[ $seenerr ]]; then
return 1
fi
}
if ! command -v realpath > /dev/null 2>&1; then
# realpath is not available on OSX unless you install the `coreutils` brew
realpath() { _realpath "$#"; }
fi
I included unit tests in my Code Review request for this code.
For those nodejs developers in a mac using bash:
realpath() {
node -p "fs.realpathSync('$1')"
}
Just an idea of using pushd:
realpath() {
eval echo "$(pushd $(dirname "$1") | cut -d' ' -f1)/$(basename "$1")"
}
The eval is used to expand tilde such as ~/Downloads.
Based on the communication with commenter, I agreed that it is very hard and has no trival way to implement a realpath behaves totally same as Ubuntu.
But the following version, can handle corner cases best answer can't and satisfy my daily needs on macbook. Put this code into your ~/.bashrc and remember:
arg can only be 1 file or dir, no wildcard
no spaces in the dir or file name
at least the file or dir's parent dir exists
feel free to use . .. / thing, these are safe
# 1. if is a dir, try cd and pwd
# 2. if is a file, try cd its parent and concat dir+file
realpath() {
[ "$1" = "" ] && return 1
dir=`dirname "$1"`
file=`basename "$1"`
last=`pwd`
[ -d "$dir" ] && cd $dir || return 1
if [ -d "$file" ];
then
# case 1
cd $file && pwd || return 1
else
# case 2
echo `pwd`/$file | sed 's/\/\//\//g'
fi
cd $last
}

Resources