I just found about zsh suffix aliases that allow one to associate a program with an extension in the shell:
alias -s {md,txt}=nano
Is there a way to do something like this but for file that do not have an extension?
I've tried:
alias -s {}=nano
But if I then try to use it, I get a command not found error:
> alias -s {}=nano
> touch file_without_extension
> file_without_extension
zsh: command not found: file_without_extension
Suffix aliases require a filename extension. You can use a command_not_found_handler function to work around that, though:
# Run this from a zsh prompt or put it in a file and source it
command_not_found_handler() {
# If just the name of an existing file is given, with no extra arguments
# open it in nano. Otherwise, print a message to stderr and return an error.
if [[ $# -eq 1 && -f $1 ]]; then
nano "$1"
else
print -u2 -f "command not found: %s\n" "$1"
return 127
fi
}
# Then with the function loaded:
$ file_without_extension # Opens in nano
$ file_that_doesnt_exist
command not found: file_that_doesnt_exist
$ file_without_extension blah
command not found: file_without_extension
base on the above answer:
with vim conceal:
command_not_found_handler() {
if [[ $# -eq 1 && -f $1 ]]; then
echo "leo test________________"
vim "$1"
else
print -u2 -f "command not found: %s\n" "$1"
# -u n Print the arguments to file descriptor n.
# 2 : stderr
return 127 # return an error.
fi
echo "aaaaaaaaaa leo test________________"
}
# 名字不能改
#
# (Nearly) All about this from man zshmisc (modified by me for easier understanding)
# If a command name contains no ✌/✌
# the shell attempts to locate it:
# If there exists a shell function by that name,
# the function is invoked as described in the section `Func‐ tions'.
# If there exists a shell ✌builtin✌ by that name,
# the builtin is invoked.
#
# contains ✌/✌
# the shell searches for a ✌directory✌ containing an executable file
# searche ✌by that name✌.
searches # in each element of $path
#
# if the search is successful
# if execution ✌fails✌ because the file is not in ✌executable format✌,
# and the file is ✌not a directory✌,
# it is assumed to be a shell script.
# /bin/sh is ✌spawned✌ to execute it.
# If the program is a file beginning with `#!',
# the remainder of the first line specifies an interpreter for the program.
# The shell will execute the specified interpreter,
# which do not handle this executable format in the ✌kernel✌ ,
# on ✌operating systems✌
#
# else (the search fails)
# the shell prints an error message and returns a nonzero exit status.
#
# If no external command is found
# but a function ✌command_not_found_handler✌ exists
# the shell executes this function with all command line arguments.
# The return status of the function becomes the status of the command.
#
# If the function wishes to mimic the behaviour of the shell when the command is not found,
# it should print the message `command not found: cmd'
# to standard error and return status 127.
#
# Note that the handler is executed in a subshell forked to execute an external command,
# hence changes to directories,
# shell parame‐ ters,
# have no effect on the main shel
#
# vim:ft=zsh
I want to write some wrappers around the sha1sum function in bash. From the manpage:
SHA1SUM(1) User Commands SHA1SUM(1)
NAME
sha1sum - compute and check SHA1 message digest
SYNOPSIS
sha1sum [OPTION]... [FILE]...
DESCRIPTION
Print or check SHA1 (160-bit) checksums.
With no FILE, or when FILE is -, read standard input.
How can I set up my wrapper so that it works in the same way? I.e.:
my_wrapper(){
# some code here
}
that could work both as:
my_wrapper PATH_TO_FILE
and
echo -n "blabla" | my_wrapper
I think this is somehow related to Redirect standard input dynamically in a bash script but not sure how to make it 'nicely'.
Edit 1
I program in a quite defensive way, so I use in my whole script:
# exit if a command fails
set -o errexit
# make sure to show the error code of the first failing command
set -o pipefail
# do not overwrite files too easily
set -o noclobber
# exit if try to use undefined variable
set -o nounset
Anything that works with that?
You can use this simple wrapper:
args=("$#") # save arguments into an array
set -o noclobber nounset pipefail errexit
set -- "${args[#]}" # set positional arguments from array
my_wrapper() {
[[ -f $1 ]] && SHA1SUM "$1" || SHA1SUM
}
my_wrapper "$#"
Note that you can use:
my_wrapper PATH_TO_FILE
or:
echo -n "blabla" | my_wrapper
This code works for me, put it in a file named wrapper
#!/bin/bash
my_wrapper(){
if [[ -z "$1" ]];then
read PARAM
else
PARAM="$1"
fi
echo "PARAM:$PARAM"
}
Load the function in your environment
. ./wrapper
Test the function with input pipe
root#51ce582167d0:~# echo hello | my_wrapper
PARAM:hello
Test the function with parameter
root#51ce582167d0:~# my_wrapper bybye
PARAM:bybye
Ok, so the answers posted here are fine often, but in my case with defensive programming options:
# exit if a command fails
set -o errexit
# exit if try to use undefined variable
set -o nounset
things do not work as well. So I am now using something in this kind:
digest_function(){
# argument is either filename or read from std input
# similar to the sha*sum functions
if [[ "$#" = "1" ]]
then
# this needs to be a file that exists
if [ ! -f $1 ]
then
echo "File not found! Aborting..."
exit 1
else
local ARGTYPE="Filename"
local PARAM="$1"
fi
else
local ARGTYPE="StdInput"
local PARAM=$(cat)
fi
if [[ "${ARGTYPE}" = "Filename" ]]
then
local DIGEST=$(sha1sum ${PARAM})
else
local DIGEST=$(echo -n ${PARAM} | sha1sum)
fi
}
I currently have a ksh script which invokes another ksh script. The "parent" ksh script needs to be invoked from a bash shell in the context of the ksh shell user. Trying the following throws back this error message
As user root in the bash shell
su - whics -c '/usr/bin/nohup /whics/t99/wv.4gm/wv99b.4gs/wv99b.sh -s 1 -m u -sleep 5 > ./nohup.out &'
/whics/t99/wv.4gm/wv99b.4gs/wv99b.sh[8]: .: wh_setENV.sh: cannot open [No such file or directory]
wh_setENV.sh is actually in /whics/t99/bin
However, when running the below commands in order I do not get this error
server:~ su - whics
server:/whics/t99 cd ./wv.4gm/wv99b.4gs
server:/whics/t99/wv.4gm/wv99b.4gs nohup ./wv99b.sh -s 1 -m u -sleep 5 &
server:/whics/t99/wv.4gm/wv99b.4gs nohup: ignoring input and appending output to `/home/whics/nohup.out'
[1] + Done nohup ./wv99b.sh -s 1 -m u -sleep 5 &
server:/whics/t99/wv.4gm/wv99b.4gs cat /home/whics/nohup.out Mon Sep 17 12:27:40 AEST 2018 : Start wv99b
wv99b.sh
#!/bin/ksh
# Copyright (C) 1992-1997 Wacher Pty. Limited
# Sccsid: %Z% %M%%Y% %Q%%I% %E%
myname=${0##*/} # a useful identifying variable
mydir=${0%$myname} # where this script is
vSFX=${myname##*.}
. wh_setENV.sh # P4813 - when using 4js:WebServices, the $fglidir/lib in LD_LIBRARY_PATH causes problems
test $debugxv && set -xv
#--------------------------------------------------------------------------------------------------------------------------------------#
wv99b_msg() {
vERR="`date` : ${vMSG}"
echo $vERR | tee -a ${vLOG}
}
#--------------------------------------------------------------------------------------------------------------------------------------#
wv99b_sysFragments() {
vSYSFRAGOK="0"
vSYSFRAGMENTS="${vTABNAME}.sysfrags.unl" ; rm -f $vSYSFRAGMENTS
$WH_ISQL $company - <<! 2>/dev/null | sed "/exprtext/d;/^$/d;s/ //g;s/[()]//g" |cut -f1 -d'=' >| ${vSYSFRAGMENTS}
select F.exprtext
from systables S, sysfragments F
where S.tabid > 99
and S.tabtype = "T"
and S.tabname = "${vTABNAME}"
and S.tabid = F.tabid
and S.tabtype = F.fragtype
and F.evalpos = 0
;
!
if [ -s ${vSYSFRAGMENTS} ] ; then
# search for the vCOLUMN in the vSYSFRAGMENTS output
vSYSFRAGOK=`grep -i ${vKEY} ${vSYSFRAGMENTS} 2>/dev/null | wc -l | awk '{print $1}'`
else
vSYSFRAGOK="0"
rm -f ${vSYSFRAGMENTS} # cleanup
fi
}
# MAIN #
vARGS="$#"
vHERE=`pwd`
vLOG="${vHERE}/errlog"
vD=0 # debug indicator
vI=0 # infile indicator
vQ=0 # email indicator
vM=0 # mode indicator
vS=0 # serial indicator
vNO_MULTI=0 # default to false
vNO_PROGI=0 # default to false
vTABLE=0 # default to 0
vSLEEP=5 # default to 0
for i in $vARGS
do
case "$i" in
-debug) vD=$2 ;;
-infile) vI=$2 ;;
-table) vTABLE=$2 ;;
-sleep) vSLEEP=$2 ;;
-no_multi) vNO_MULTI=$2 ;;
-no_progi) vNO_PROGI=$2 ;;
-m) vM=$2 ;;
-q) vQ=$2 ;;
-s) vS=$2 ;;
esac
shift
done
[[ ${vS} -eq 0 ]] && vMSG="-s parameter not supplied" && wv99b_msg && exit 1
vHERE=`pwd`
if [ ${vD} -eq 1 ] ; then
vDEBUG=" -debug 1"
else
vDEBUG=""
fi
if [ ${vI} -eq 0 ] ; then
vINFILE="wv99b.in"
else
vINFILE="${vI}"
fi
# INIT
vWVI="wv99b_I" # the name of the (I)dentify script
vWVIS="${vWVI}_${vS}" # the name of the (I)dentify script PLUS SERIAL
vWVIO="${vWVIS}.unl" # the name of the (I)dentify script
rm -f ${vWVIO}
# Check that transaction-logging is off
# check that vINFILE exists
if [ ! -s "${vINFILE}" ] ; then
vMSG="Error cannot read input file $vINFILE" ; wv99b_msg ; exit 1
fi
# Process only one(1) table
if [ ${vTABLE} != "0" ] ; then
vTABLE_FILTER=" -table ${vTABLE} "
else
vTABLE_FILTER=""
fi
# We need to check if we are running client/server
#
vDB=`echo $company | awk 'BEGIN {FS="#" } { print $1 }'`
vDBSRV=`echo $company | awk 'BEGIN {FS="#" } { print $2 }'`
case X${vDBSRV}X in
XX) vREMOTE_DB="" ;;
*) vREMOTE_DB=" -db ${vDB} -dbsrv ${vDBSRV} " ;;
esac
#_end
vMSG="Start wv99b" ; wv99b_msg
So in the wv99b.sh file, I changed
. wh_setENV.sh
to
. /whics/t99/bin/wh_setENV.sh
However, now I get the error
cannot read input file wv99b.in
I checked wv99b.in and it is in the same directory as 'wv99b.sh' (i.e. /whics/t99/wv.4gm/wv99b.4gs/ )
wh_setENV.sh
#!/usr/bin/ksh
test $debugxv && set -xv
trap door 1 2 3 5 9 15
#---------------------------------------------------------------------#
door() {
echo "`date` ERROR($?) occured in $0" >> $WH/batch.4gm/trap.log
} #end door
#---------------------------------------------------------------------#
# Script to set Environment variables for various scripts
# Stef
# Unix specific
umask 002
: ${WH:="/whics/prod"}
set -a
TERM=xterm
vHERE=`pwd`
TERMCAP=$WH/etc/termcap
vHOST=`hostname | cut -f1 -d'.'`
set +a
#LD_LIBRARY_PATH="$WH/lib.4gm/S_lib:$fglibdir/S_lib" # GUC R481
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$INFORMIXDIR/lib/c++:$INFORMIXDIR/lib/cli:$INFORMIXDIR/lib/client:$INFORMIXDIR/lib/csm:$INFORMIXDIR/lib/dmi"
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$INFORMIXDIR/lib:$INFORMIXDIR/lib/esql:$INFORMIXDIR/lib/tools.$CCODE"
export LD_LIBRARY_PATH
# EOF #
UPDATE: After OP modified/updated the question ...
wv99b.in is referenced in wv99b.sh
you don't provide the path for wv99b.in so ...
if you invoke wv99b.sh from some directory other than /whics/t99/wv.4gm/wv99b.4gs ...
when you run your test [ ! -s "${vINFILE}" ] your script (correctly) determines that "${vINFILE}" is not located in the directory where you invoked wv99b.sh from
Net result ... the original problem with sh_setENV.sh has been fixed but now the same exact problem is occurring for wv99b.in, with the same solution needed here ... invoke wv99b.sh from its home directory or provide the full path to wv99b.in.
ORIGINAL POST:
Expanding on Andre's comment ...
In wv99b.sh you have the following line:
. wh_setENV.sh
This is going to look for wh_setENV.sh in the directory where wv99b.sh is invoked from.
In your original example you've provided the full path to wv99b.sh but the question is ... what directory is that call invoked from? We can tell from the error ...
wh_setENV.sh: cannot open [No such file or directory]
... that wv99b.sh was not invoked from /whics/t99/wv.4gm/wv99b.4gs otherwise it would have found wh_setENV.sh.
From your second example it appears that the full path to wh_setENV.sh is: /whics/t99/wv.4gm/wv99b.4gs/wh_setENV.sh so you have a couple options:
in your initial example make sure you cd /whics/t99/wv.4gm/wv99b.4gs before executing wv99b.4gs
or
update wv99b.4gs to include the full path to the location of sh_setENV.sh, eg:
. /whics/t99/wv.4gm/wv99b.4gs/wh_setENV.sh
I am missing my bash aliases in fish, and don't want to manually convert all of them to fish functions.
How to get them all from bash to fish?
Bonus points if:
the solution supports an iterative process, as in: i can easily change the aliases in bash, and re-convert/re-import them into fish
the solution also imports bash functions
Convert bash aliases to bash scripts
I decided to do this instead of approach below, and putting the scripts into ~/bin/, which is in my PATH. This allows me to use them from whatever shell i am currently executing, and it prevents potential problems with quoting.
use like so:
# converts all bash aliases to script files
convert_bash_aliases_to_scripts
# removes all scripts previously converted by this script
convert_bash_aliases_to_scripts clean
bash conversion script:
#!/bin/bash
# Convert bash aliases to bash scripts.
#
# Copyright 2018 <hoijui.quaero#gmail.com>, licensed under the GPL-3.0+
#
# Usage:
# convert_bash_aliases_to_scripts # converts all bash aliases to script files
# convert_bash_aliases_to_scripts clean # removes all scripts previously converted by this script
COLOR_RED=$'\e[0;31m'
COLOR_ORANGE=$'\e[0;33m'
COLOR_BLUE=$'\e[0;34m'
COLOR_BLUE_LIGHT=$'\e[1;34m'
COLOR_GREEN=$'\e[0;32m'
COLOR_BROWN=$'\e[0;33m'
COLOR_YELLOW=$'\e[1;33m'
COLOR_WHITE=$'\e[1;37m'
COLOR_CYAN=$'\e[0;36m'
COLOR_PURPLE=$'\e[0;35m'
COLOR_GRAY=$'\e[1;30m'
COLOR_GRAY_LIGHT=$'\e[0;37m'
COLOR_NONE=$'\e[m' # No Color
OUTPUT_DIR=~/bin/converted/aliases
LINKS_DIR=~/bin
README_FILE_NAME="README.md"
README_FILE="$OUTPUT_DIR/$README_FILE_NAME"
if [ "$1" = "clean" ]
then
for script_file in $(find "$LINKS_DIR" -maxdepth 1 -type l)
do
conv_script_file="$OUTPUT_DIR/$(basename $script_file)"
if [ -e $conv_script_file ] && [ "$(readlink --canonicalize $script_file)" = "$(realpath $conv_script_file)" ]
then
script_name=$(basename $script_file)
echo "removing converted bash alias-script: $script_name"
rm $conv_script_file \
&& rm $script_file
fi
done
rm $README_FILE 2> /dev/null
rmdir $OUTPUT_DIR 2> /dev/null
exit 0
fi
SOURCE_FILES="${HOME}/.bashrc ${HOME}/.bash_aliases"
mkdir -p $OUTPUT_DIR
echo -e "# Bash alias conversion scripts\n\nsee $0\n\nWARNING: Do NOT manually edit files in this directory. instead, copy them to $LINKS_DIR (replacing the symbolic link that already exists there), and edit that new file.\nIf you edit the files in this dir, it will be replaced on the next (re)conversion from aliases." \
> $README_FILE
AUTO_IMPORT_WARNING="# WARNING Do NOT edit this file by hand, as it was auto-generated from a bash alias, and may be overwritten in the future. please read ${README_FILE}"
function _is_link_to {
local file_link=$1
local file_target=$2
test -e $file_target \
&& test "$(readlink --canonicalize $file_link)" = "$(realpath $file_target)"
return $?
}
for source_file in $SOURCE_FILES
do
IFS=$'\n'
for a in $(cat $source_file | grep "^alias")
do
a_name="$(echo "$a" | sed -e 's/alias \([^=]*\)=.*/\1/')"
a_command="$(echo "$a" | sed -e 's/alias \([^=]*\)=//' -e 's/[ \t]*#.*$//')"
if echo "${a_command:0:1}" | grep -q -e "[\'\"]"
then
# unquote
a_command_start=1
let a_command_end="${#a_command} - 2"
else
# leave as is
a_command_start=0
let a_command_end="${#a_command}"
fi
script_file="$LINKS_DIR/$a_name"
conv_script_file="$OUTPUT_DIR/$a_name"
# Check whether the script already exists.
# If so, we skip importing it, unless it is just a link to a previously imported script.
log_action="none"
log_action_color="${COLOR_NONE}"
log_content=""
if [ -e $script_file ] && ! $(_is_link_to $script_file $conv_script_file)
then
log_action="skipped (exists)"
log_action_color="${COLOR_ORANGE}"
log_content=""
else
if [ -e $script_file ]
then
log_action="reimporting"
log_action_color="${COLOR_BLUE}"
else
log_action="importing"
log_action_color="${COLOR_GREN}"
fi
# write the script file to a temporary location
conv_script_file_tmp="${conv_script_file}_BAK"
echo "#!/bin/bash" > $conv_script_file_tmp
echo -e "$AUTO_IMPORT_WARNING" >> $conv_script_file_tmp
echo -e "#\n# Imported bash alias '$a_name' from file '$source_file'" >> $conv_script_file_tmp
cat >> "${conv_script_file_tmp}" <<EOF
${a_command:${a_command_start}:${a_command_end}} \${#}
EOF
if diff -N ${conv_script_file_tmp} ${conv_script_file} > /dev/null
then
log_content="no change"
log_content_color="${COLOR_NONE}"
else
log_content="changed"
log_content_color="${COLOR_GREEN}"
fi
log_content=$(printf "%s %10s -> %s${COLOR_NONE}" "${log_content_color}" "${log_content}" "$a_command")
mv "${conv_script_file_tmp}" "${conv_script_file}"
# make the script executable
chmod +x $conv_script_file
# remove the link if it already exists (in case of reimport)
rm $script_file 2> /dev/null
# .. and re-create it as local symbolic link
# to the function in the imports dir
ln --symbolic --relative $conv_script_file $script_file
fi
printf "%s%20s: %-25s${COLOR_NONE}%s\n" "${log_action_color}" "${log_action}" "$a_name" "${log_content}"
done
done
Deprecated: Creating fish wrappers that execute bash code
Below is a script that creates fish script wrappers for the local bash aliases: For each bash alias, it takes the contents, and creates a fish alias/script that executes the code in bash sub-shell.
It is not optimal, but is sufficient for most of my aliases.
WARNING It might happen that the imported function acts differently then in bash. You may loose data or accidentally DDOS your coworkers when using them.
use like so:
# imports (or reimports) all bash aliases into fish functions, permanently
import_bash_aliases
# removes all fish functions previously imported by this script
import_bash_aliases clean
save this in ~/.config/fish/functions/import_bash_aliases.fish:
#!/usr/bin/fish
# Fish function to import bash aliases
#
# Copyright 2018 <hoijui.quaero#gmail.com>, licensed under the GPL-3.0+
#
# This script is based on a script from Malte Biermann,
# see: https://glot.io/snippets/efh1c4aec0
#
# WARNING: There is no guarantee that the imported aliases work the same way
# as they do in bash, so be cautious!
#
# Usage:
# import_bash_aliases # imports (or reimports) all bash aliases into fish functions, permanently
# import_bash_aliases clean # removes all fish functions previously imported by this script from bash aliases
function import_bash_aliases --description 'Converts bash aliases to .fish functions.\nThis might be called repeatedly, and will not override functions that are already defined in fish, except they are merely an older import from this script.'
set -l FISH_FUNCTIONS_DIR ~/.config/fish/functions
set -l BASH_IMPORTS_DIR_NAME bash-imports
set -l BASH_IMPORTS_DIR $FISH_FUNCTIONS_DIR/$BASH_IMPORTS_DIR_NAME
set -l README_FILE $BASH_IMPORTS_DIR/README.md
if test "$argv[1]" = "clean"
for fun_file in (find $FISH_FUNCTIONS_DIR -maxdepth 1 -name '*.fish')
set -l imp_fun_file $BASH_IMPORTS_DIR/(basename $fun_file)
if test -e $imp_fun_file ; and test (readlink --canonicalize $fun_file) = (realpath $imp_fun_file)
set -l fun_name (basename $fun_file '.fish')
echo "removing imported bash alias/function $fun_name"
rm $imp_fun_file
and rm $fun_file
and functions --erase $fun_name
end
end
rm $README_FILE ^ /dev/null
rmdir $BASH_IMPORTS_DIR ^ /dev/null
return 0
end
set -l SOURCE_FILES ~/.bashrc ~/.bash_aliases
mkdir -p $BASH_IMPORTS_DIR
echo -e "# Bash alias imports\n\nsee `$argv[0]`\n\nWARNING: Do NOT manually edit files in this directory. instead, copy them to $FISH_FUNCTIONS_DIR (replacing the symbolic link that already exists there), and edit that new file.\nIf you edit the files in this dir, it will be replaced on the next (re)import from bash aliases." \
> $README_FILE
set -l UNUSED_STUB_MSG "The bash alias corresponding to this function was NOT imported, because a corresponding function already exists at %s\n"
set -l AUTO_IMPORT_WARNING "# WARNING Do NOT edit this file by hand, as it was auto-generated from a bash alias, and may be overwritten in the future. please read {$README_FILE}"
function _fish_func_exists
set -l fun_name $argv[1]
# This also detects in-memory functions
functions --query $fun_name
# This also detects script files in the functions dir
# that do not contain a function wiht the same name
or test -e "$FISH_FUNCTIONS_DIR/$fun_name.fish"
return $status
end
function _is_link_to
set -l file_link $argv[1]
set -l file_target $argv[2]
test -e $file_target
and test (readlink --canonicalize $file_link) = (realpath $file_target)
return $status
end
for source_file in $SOURCE_FILES
for a in (cat $source_file | grep "^alias")
set -l a_name (echo $a | sed -e 's/alias \([^=]*\)=.*/\1/')
set -l a_command (echo $a | sed -e 's/alias \([^=]*\)=//' -e 's/[ \t]*#[^\'\"]\+$//')
set -l fun_file "$FISH_FUNCTIONS_DIR/$a_name.fish"
set -l imp_fun_file "$BASH_IMPORTS_DIR/$a_name.fish"
# Check whether the function already exists.
# If so, we skip importing it, unless it is just a link to a previously imported function.
if _fish_func_exists $a_name; and not _is_link_to $fun_file $imp_fun_file
set_color red
printf "%20s: %-25s\n" "skipping (exists)" $a_name
set_color normal
#printf $UNUSED_STUB_MSG $fun_file > $imp_fun_file
else
set_color green
printf "%20s: %-25s -> %s\n" "(re-)importing" $a_name $a_command
set_color normal
# remove the link, in case of re-importing
rm $fun_file ^ /dev/null
# write the function file
echo "#!/usr/bin/fish" > $imp_fun_file
echo "\
$AUTO_IMPORT_WARNING
function $a_name -d 'bash alias "$a_name" import'
bash -c $a_command' '\$argv''
end
" \
>> $imp_fun_file
# make the script executable
chmod +x $imp_fun_file
# .. and re-create it as local symbolic link
# to the function in the imports dir
ln --symbolic --relative $imp_fun_file $fun_file
end
end
end
# (re-)load all the functions we just defined
exec fish
end
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
}