Context
I have an arduino-cli wrapper script named ino that reads target/build configuration from:
JSON files located in the sketch directory
Command-line flags/arguments
It then constructs and exec's the corresponding arduino-cli command-line.
Problem
As a convenience wrapper script, ino isn't intended to support every feature of arduino-cli. So for those tasks that ino doesn't automate, the user can instead invoke arduino-cli indirectly using the cli subcommand of ino.
For example, if the user types the following commands:
% ino cli update
% ino cli core list --all
The ino script will take everything following cli and simply append them to the arduino-cli executable. So they would be equivalent to the following commands:
% arduino-cli update
% arduino-cli core list --all
Since arduino-cli has nice bash completion for all of its subcommands and flags, I would like to hijack the same completion functionality for my ino cli subcommand.
What I've tried
The accepted answers here:
How do I autocomplete nested, multi-level subcommands?
Multi Level Bash Completion
These helped me understand how to identify the current subcommand and discriminate the completion results based upon it.
However, I couldn't figure out how to then invoke the arduino-cli completion handler using the remaining args.
Completion handler derived from accepted answer here:
How do I get bash completion for command aliases?
See my ino completion handler based on that answer below (Reference 1).
This question/answer isn't quite the same, because they can basically just install a completion handler on their alias. I'm needing to "install" one on an argument to a command/alias.
This almost seems to work. Try it with xtrace option enabled (set -x), and you can see the arduino-cli command-line is appearing in the args ... but following ino at position $0.
E.g., given ino cli core list --all to the wrapper handler, the arduino-cli handler receives ino arduino-cli core list --all. Not sure how to get rid of $0!
Reference
ino completion wrapper derived from alias-based wrappers
joinstr() {
local d=${1-} f=${2-}
shift 2 && printf %s "$f" "${#/#/$d}"
}
complete-subcmd() {
[[ ${#} -gt 2 ]] || {
printf "usage:\n\tcomplete-subcmd src-command... -- comp-func dst-command...\n"
return 1
}
# parse the command-line by splitting it into two command-lines
# of variable length, src-command and dst-command:
# 1. src-command is the trigger that invokes the real completion
# handler, comp-func.
# 2. dst-command is the leading args of the command-line passed
# to the real completion handler, comp-func, to produce the
# resulting completion choices.
unset -v dstparse
local -a srccmd dstcmd
local func
while [[ ${#} -gt 0 ]]; do
case "${1}" in
--)
# when we reach the delimiter, also shift in comp-func as
# the next argument (the real completion handler).
dstparse=1
shift
func=${1:-}
;;
*)
# if we aren't processing the delimiter, then all other
# args are appended to either src-command or dst-command.
if [[ -z ${dstparse} ]]; then
srccmd+=( "${1}" )
else
dstcmd+=( "${1}" )
fi
;;
esac
shift
done
# if the completer is dynamic and not yet loaded, try to load it
# automatically using the given command
if [[ $( type -t "${func}" ) != function ]]; then
type -p _completion_loader &> /dev/null &&
_completion_loader "${dstcmd[#]}"
fi
local wrap=$( joinstr _ "${srccmd[#]}" | tr -d -c '[A-Za-z_]' )
# replace our args with dst-command followed by whatever remains
# from the invoking command-line.
eval "
function _${wrap} {
(( COMP_CWORD+=$(( ${#dstcmd[#]} )) ))
COMP_WORDS=( "${dstcmd[#]}" \${COMP_WORDS[#]:1} )
"${func}"
return 0
}
"
# install this wrapper handler on the first word in src-command
complete -F "_${wrap}" "${srccmd[0]}"
}
complete-subcmd ino cli -- __start_arduino-cli arduino-cli
completion.bash from arduino-cli
# bash completion V2 for arduino-cli -*- shell-script -*-
__arduino-cli_debug()
{
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
fi
}
# Macs have bash3 for which the bash-completion package doesn't include
# _init_completion. This is a minimal version of that function.
__arduino-cli_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$#" cur prev words cword
}
# This function calls the arduino-cli program to obtain the completion
# results and the directive. It fills the 'out' and 'directive' vars.
__arduino-cli_get_completion_results() {
local requestComp lastParam lastChar args
# Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly arduino-cli allows to handle aliases
args=("${words[#]:1}")
requestComp="${words[0]} __completeNoDesc ${args[*]}"
lastParam=${words[$((${#words[#]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__arduino-cli_debug "lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
__arduino-cli_debug "Adding extra empty parameter"
requestComp="${requestComp} ''"
fi
# When completing a flag with an = (e.g., arduino-cli -n=<TAB>)
# bash focuses on the part after the =, so we need to remove
# the flag part from $cur
if [[ "${cur}" == -*=* ]]; then
cur="${cur#*=}"
fi
__arduino-cli_debug "Calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
# Extract the directive integer at the very end of the output following a colon (:)
directive=${out##*:}
# Remove the directive
out=${out%:*}
if [ "${directive}" = "${out}" ]; then
# There is not directive specified
directive=0
fi
__arduino-cli_debug "The completion directive is: ${directive}"
__arduino-cli_debug "The completions are: ${out[*]}"
}
__arduino-cli_process_completion_results() {
local shellCompDirectiveError=1
local shellCompDirectiveNoSpace=2
local shellCompDirectiveNoFileComp=4
local shellCompDirectiveFilterFileExt=8
local shellCompDirectiveFilterDirs=16
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
# Error code. No completion.
__arduino-cli_debug "Received error from custom completion go code"
return
else
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__arduino-cli_debug "Activating no space"
compopt -o nospace
else
__arduino-cli_debug "No space directive not supported in this version of bash"
fi
fi
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__arduino-cli_debug "Activating no file completion"
compopt +o default
else
__arduino-cli_debug "No file completion directive not supported in this version of bash"
fi
fi
fi
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering
local fullFilter filter filteringCmd
# Do not use quotes around the $out variable or else newline
# characters will be kept.
for filter in ${out[*]}; do
fullFilter+="$filter|"
done
filteringCmd="_filedir $fullFilter"
__arduino-cli_debug "File filtering command: $filteringCmd"
$filteringCmd
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
# File completion for directories only
# Use printf to strip any trailing newline
local subdir
subdir=$(printf "%s" "${out[0]}")
if [ -n "$subdir" ]; then
__arduino-cli_debug "Listing directories in $subdir"
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
else
__arduino-cli_debug "Listing directories in ."
_filedir -d
fi
else
__arduino-cli_handle_standard_completion_case
fi
__arduino-cli_handle_special_char "$cur" :
__arduino-cli_handle_special_char "$cur" =
}
__arduino-cli_handle_standard_completion_case() {
local tab comp
tab=$(printf '\t')
local longest=0
# Look for the longest completion so that we can format things nicely
while IFS='' read -r comp; do
# Strip any description before checking the length
comp=${comp%%$tab*}
# Only consider the completions that match
comp=$(compgen -W "$comp" -- "$cur")
if ((${#comp}>longest)); then
longest=${#comp}
fi
done < <(printf "%s\n" "${out[#]}")
local completions=()
while IFS='' read -r comp; do
if [ -z "$comp" ]; then
continue
fi
__arduino-cli_debug "Original comp: $comp"
comp="$(__arduino-cli_format_comp_descriptions "$comp" "$longest")"
__arduino-cli_debug "Final comp: $comp"
completions+=("$comp")
done < <(printf "%s\n" "${out[#]}")
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${completions[*]}" -- "$cur")
# If there is a single completion left, remove the description text
if [ ${#COMPREPLY[*]} -eq 1 ]; then
__arduino-cli_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
comp="${COMPREPLY[0]%% *}"
__arduino-cli_debug "Removed description from single completion, which is now: ${comp}"
COMPREPLY=()
COMPREPLY+=("$comp")
fi
}
__arduino-cli_handle_special_char()
{
local comp="$1"
local char=$2
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
local word=${comp%"${comp##*${char}}"}
local idx=${#COMPREPLY[*]}
while [[ $((--idx)) -ge 0 ]]; do
COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"}
done
fi
}
__arduino-cli_format_comp_descriptions()
{
local tab
tab=$(printf '\t')
local comp="$1"
local longest=$2
# Properly format the description string which follows a tab character if there is one
if [[ "$comp" == *$tab* ]]; then
desc=${comp#*$tab}
comp=${comp%%$tab*}
# $COLUMNS stores the current shell width.
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
maxdesclength=$(( COLUMNS - longest - 4 ))
# Make sure we can fit a description of at least 8 characters
# if we are to align the descriptions.
if [[ $maxdesclength -gt 8 ]]; then
# Add the proper number of spaces to align the descriptions
for ((i = ${#comp} ; i < longest ; i++)); do
comp+=" "
done
else
# Don't pad the descriptions so we can fit more text after the completion
maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
fi
# If there is enough space for any description text,
# truncate the descriptions that are too long for the shell width
if [ $maxdesclength -gt 0 ]; then
if [ ${#desc} -gt $maxdesclength ]; then
desc=${desc:0:$(( maxdesclength - 1 ))}
desc+="…"
fi
comp+=" ($desc)"
fi
fi
# Must use printf to escape all special characters
printf "%q" "${comp}"
}
__start_arduino-cli()
{
local cur prev words cword split
COMPREPLY=()
# Call _init_completion from the bash-completion package
# to prepare the arguments properly
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -n "=:" || return
else
__arduino-cli_init_completion -n "=:" || return
fi
__arduino-cli_debug
__arduino-cli_debug "========= starting completion logic =========="
__arduino-cli_debug "cur is ${cur}, words[*] is ${words[*]}, #words[#] is ${#words[#]}, cword is $cword"
# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $cword location, so we need
# to truncate the command-line ($words) up to the $cword location.
words=("${words[#]:0:$cword+1}")
__arduino-cli_debug "Truncated words[*]: ${words[*]},"
local out directive
__arduino-cli_get_completion_results
__arduino-cli_process_completion_results
}
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_arduino-cli arduino-cli
else
complete -o default -o nospace -F __start_arduino-cli arduino-cli
fi
# ex: ts=4 sw=4 et filetype=sh
UPDATE2:
After I posted this I checked your links and after seeing the accepted answer here, I was thinking I am just old and forget that I just copied this code from that link, and it wasn't me who wrote it. Even the example used there is the same, but investigating the code further, it looks I did write this and used a different approach, and maybe this will help you understand what's going on. As I mentioned at the bottom UPDATE1 section: you need to tune the COMP variables then call the original function
Original:
I wrote an 'alias wrapper' script a couple of years ago.
The idea is to use the original bash completion with aliases even with parameters.
For example:
alias apti='apt-get install'
source alias-completion-wrapper _apt_get apti apt-get install
#here _apt_get is the original completion function
Now you can use tab to complete the package name after apti just like after apt-get install
#alias-completion-wrapper
#Example: . alias-completion-wrapper _apt_get apti apt-get install
comp_function_name="$1"
ali="$2"
shift 2
x="$#"
function_name=`echo _$# |tr ' ' _`
function="
function $function_name {
_completion_loader $1
(( COMP_CWORD += $# - 1 ))
COMP_WORDS=( $# \"\${COMP_WORDS[#]:1}\")
COMP_LINE=\"\${COMP_WORDS[#]}\"
let COMP_POINT=\${COMP_POINT}-${#ali}+${#x}
$comp_function_name
return 0
}"
eval "$function"
complete -F $function_name $ali
unset function function_name ali x
To be honest, I can't remember how it works and I didn't commented the script :)
But I think you will be able to tune this for your needs.
UPDATE1:
As I investigated the code a bit, it looks like the idea is to tune the COMP variables, then call the original function :)
UPDATE3:
I had some time, so the modification you need are:
This ${COMP_WORDS[#]} contains the current command line. ${COMP_WORDS[#]:1} cuts off the first word, which is originally the alias/command. As you want to use it after a parameter you have to cut off the parameter too.
COMP_WORDS=( $# \"\${COMP_WORDS[#]:2}\")
I don't see COMP_LINE and COMP_POINT in the other solution, but as I can recall without those, it doesn't worked well in certain circumstances. So I suppose you need:
COMP_LINE=\"\${COMP_WORDS[#]:1}\"
And here ${#ali} is the length of the command. You need to replace this with the length of your command with the parameter. eg,:"xcmd prm" -> 8 (count the space too)
let COMP_POINT=\${COMP_POINT}-${#ali}+${#x}
Not sure about (( COMP_CWORD += $# - 1 )) either remove the -1 or use -2 or leave it as it is :)
After the modifications, just change the eval to echo and remove the complete -F line. And source the script as described. This way it will echo the function what you can insert into your completion script.
I am creating a setup bash script so I can quickly setup my servers.
Inside the run function I want to pass the command, which the run function will then verify whether is successful.
function run () {
if output=$( $1 );
then printf 'OK. («%s»)\n' "$output";
else printf 'Failed! («%s»)\n' "$output";
fi
}
printf 'Setting up «uni» as system group...'
run " if [ ! $( getent group uni ) ];
then sudo addgroup --system uni;
else echo 'Group exists.';
fi "
However this results in a error: setup.sh: line 5: if: command not found
When I do this it works fine, but I want to eliminate repetitive code as I ha ve many commands:
if output=$(
if [ ! $( getent group uni ) ]; then sudo addgroup --system uni; else echo 'Group exists.'; fi
);
then printf 'OK. («%s»)\n' "$output";
else printf 'Failed! («%s»)\n' "$output";
fi
What am I doing wrong?
The safe way to pass code to functions is... to encapsulate that code in another function.
run() {
local output
if output=$("$#"); then
printf 'OK. («%s»)\n' "$output";
else
printf 'Failed! («%s»)\n' "$output";
fi
}
printf 'Setting up «uni» as system group...'
step1() {
if [ ! "$(getent group uni)" ]; then
sudo addgroup --system uni;
else
echo 'Group exists.';
fi
}
run step1
If your code didn't involve flow control operators (and otherwise fit into the definition of a "simple command"), you wouldn't even need to do that; with the above run definition (using "$#" instead of $1),
run sudo addgroup --system uni
...would work correctly as-is.
Using either eval or sh -c exposes you to serious security problems; see BashFAQ #48 for a high-level overview, and see BashFAQ #50 for a discussion of why code shouldn't be passed around as text (and preferred ways to avoid the need to do so in the first place!)
You have chosen to pass a shell command string to your function (aka system(3) semantics). This means that you have to use eval to evaluate it:
function run () {
if output=$( eval "$1" );
then printf 'OK. («%s»)\n' "$output";
else printf 'Failed! («%s»)\n' "$output";
fi
}
Note that the parameter with your command will be expanded as normal, so if you want the $(getent) to be evaluated in the run function instead of before it, you will need to escape it:
run " if [ ! \$( getent group uni ) ];
then sudo addgroup --system uni;
else echo 'Group exists.';
fi "
If you want to pass a bash piece of code as a function args to be evaluated in a subshell and test the success, you can do it like that:
#!/bin/bash
run () {
# Here I'm not sure what you want to test, so I assume that you want to test if the piece of bash pass as an arg is failing or not
# Do not forget to quote the value in order to take all the string
# You can also try with "eval" instead of "bash -c"
if output=$( bash -c "${1}" ); then
echo "OK. («${output}»)"
else
echo "Failed! («${output}»)"
fi
}
# Do not forget to escape the $ here to be evaluated after in the `run` function
# Do not forget to exit with a code different of 0 to indicate there is a failure for the test in the "run" function
run "if [[ ! \$( getent group uni ) ]]; then sudo addgroup --system uni else echo 'Group exists.'; exit 1; fi"
Warning: I posted this piece of code to make your script works the way you seems to want (have a function which will eval your code from a string and interpret the result) but it's not safe at all for this reasons as Charles Duffy pointed out in the comment section.
It's probably safer to make something like:
#!/bin/bash
run () {
if output=$("${#}"); then
echo "OK. («${output}»)"
else
echo "Failed! («${output}»)"
fi
}
one_of_your_functions_you_want_to_eval() {
if [[ ! $( getent group uni ) ]]; then
sudo addgroup --system uni
else
echo 'Group exists.'
# do not forget to exit with something different than 0 here
exit 1
fi
}
run one_of_your_functions_you_want_to_eval
Note: in order to declare a function, either you declare it like that (POSIX compliant syntax):
your_function() {
}
Either using a bash only syntax (I think it's better to avoid "bashism" when it not bring real values to your scripts):
function your_function {
}
But no need to mix both syntaxes ;)
I'm currently creating monstrosities like the following:
ll /home && echo -e "==============\n" && getent passwd && echo -e "==============\n" && ll /opt/tomcat/ && echo -e "==============\n" && ll /etc/sudoers.d/
Is there perhaps some program that handles this in a nicer way?
Something like this (the hypothetical name of the program would be multiprint in my example):
multiprint --delim-escapechars true --delim "============\n" '{ll /home},{getent passwd},...'
alternatively:
multiprint -de "============\n" '{ll /home},{getent passwd},...'
A function like the following would give you that ability :
function intersect() {
delim=$1
shift
for f; do cat "$f"; echo "$delim"; done
}
You could call it as follows to implement your specific use-case :
intersect '==============' <(ll /home) <(getent passwd) <(ll /opt/tomcat/) <(ll /etc/sudoers.d/)
You can try it here.
printf will repeat its format until its arguments are exhausted. You could write something like
printf '%s\n================\n' "$(ll /home)" "$(getent passed)" "$(ll /opt/tomcat)" "$(ll /etc/sudoers.d)"
although this is a little memory-intensive, since it buffers all the output in memory until all the commands have completed.
Based on #Aaron's answer I ended up creating this multiprint.sh Bash shell script, and for what it's worth posting it here:
#!/bin/bash
# Print output of multiple commands, delimited by a specified string
function multiprint() {
if [[ -z "$*" ]]; then
__multiprint_usage
return 0
elif [[ "$1" == "--help" ]]; then
__multiprint_usage
return 0
else
delim=$1
shift
for f; do cat "$f"; echo -e "$delim"; done
fi
}
function __multiprint_usage() {
echo "Usage:"
echo " multiprint '<delimiter>' <(cmd1) <(cmd2) ..."
# example: multiprint '\n\n\n' <(ll /home/) <(ll /var/) ..."
}
* the wording of the question is terrible, sorry!
I have some bash functions I create
test() {echo "hello wold"}
test2() {echo "hello wold"}
Then in my .bashrc I source the file that has the above function . ~/my_bash_scripts/testFile
In the terminal I can run test and get hello world.
is there a way for me to add parent variable that holds all my functions together. For example personal test, personal test2.
Similar to every other gem out there, I downloaded a tweeter one. All it's methods are followed by the letter t, as in t status to write a status, instead of just status
You are asking about writing a command-line program. Just a simple one here:
#!/usr/bin/env bash
if [[ $# -eq 0 ]]; then
echo "no command specified"
exit
elif [[ $# -gt 1 ]]; then
echo "only one argument expected"
exit
fi
case "$1" in
test)
echo "hello, this is test1"
;;
test2)
echo "hello, this is test2"
;;
*)
echo "unknown command: $1"
;;
esac
Then save it and make it an executable by run chmod +x script.sh, and in your .bashrc file, add alias personal="/fullpath/to/the/script.sh".
This is just very basic and simple example using bash and of course you can use any language you like, e.g. Python, Ruby, Node e.t.c.
Use arguments to determine final outputs.
You can use "$#" for number of arguments.
For example,
if [ $# -ne 2 ]; then
# TODO: print usage
exit 1
fi
Above code exits if arguments not euqal to 2.
So below bash program
echo $#
with
thatscript foo bar baz quux
will output 4.
Finally you can combine words to determine what to put stdout.
If you want to flag some functions as your personal functions; no, there is no explicit way to do that, and essentially, all shell functions belong to yourself (although some may be defined by your distro maintainer or system administrator as system-wide defaults).
What you could do is collect the output from declare -F at the very top of your personal shell startup file; any function not in that list is your personal function.
SYSFNS=$(declare -F | awk '{ a[++i] = $3 }
END { for (n=1; n<=i; n++) printf "%s%s", (n>1? ":" : ""), a[n] }')
This generates a variable SYSFNS which contains a colon-separated list of system-declared functions.
With that defined, you can check out which functions are yours:
myfns () {
local fun
declare -F |
while read -r _ _ fun; do
case :$SYSFNS: in *:"$fun":*) continue;; esac
echo "$fun"
done
}
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
}