Avoid trimming of bash $() output - bash

From the Bash manual:
Bash performs the expansion by
executing command and replacing the
command substitution with the standard
output of the command, with any
trailing newlines deleted.
That means obscure bugs are possible when handling output with meaningful trailing newlines. A contrived example:
user#host:~$ path='test
'
user#host:~$ touch -- "$path"
user#host:~$ readlink -fn -- "$path"
/home/user/test
user#host:~$ full_path="$(readlink -fn -- "$path")"
user#host:~$ ls -- "$full_path"
ls: cannot access /home/user/test: No such file or directory
Any tips on how to assign the value of a command to a variable without losing semantically useful data?
The adventures of Bash continue another day!

You could use quoting and eval to work around this. Change your last two commands to:
full_path="'$(readlink -fn -- "$path"; echo \')"
eval ls -- "$full_path"
If you want the result with trailing newlines in a variable you could first add a bogus character (in this case underscore) and then remove that.
full_path="$(readlink -fn -- "$path"; echo _)"
full_path=${full_path%_}
ls -- "$full_path"

I have no good answers. However, this hack will work for both files with and without newlines in the name.
path='test
'
touch -- "$path"
readlink -fn -- "$path"
full_path=
if [[ $path =~ $'\n' ]] ; then
while IFS=$'\n' read fn ; do
full_path+="$fn"$'\n'
done < <(readlink -fn -- "$path")
else
full_path="$(readlink -fn -- "$path")"
fi
ls -l -- "$full_path"

One (deprecated) way could be to use back-quotes instead of $(); try:
full_path=`readlink -fn -- $path`

$ readlink -f "$path"
This return the good path. I don't know why you used the -n option with readlink because it removes newlines.
Unfortunatly when I store the result of this command the newline seems to be removed
$ fullpath=`readlink -f "$path"`
$ echo "$fullpath"
/home/test
No newlines. I don't know if it's "echo" or the way to store that remove the newline at the end of the path.
$ ls
test?
$ls test?
test?
Newline seems to be replaced by ?. May be there is something to do with it.

Related

source command is not working with variables or quoted strings

I am having a problem with this.
If I do...
source /Users/cristian/Proyectos/MikroTik\ Updater/sources/testfile
It does work
If I do...
source "/Users/cristian/Proyectos/MikroTik\ Updater/sources/testfile"
It doesn’t
The problem is that I’m using a variable which contains a path got some steps before
So this...
mypath="/Users/cristian/Proyectos/MikroTik\ Updater/sources/testfile"
source $mypath
Doesn’t work neither
I found a workaround doin...
eval "source $mypath"
But of course it is a big security hole because file name comes from an argument
What can I do?
EDIT:
As you can see in the code I echo the filename and then try to source it
updaterpath="$( cd "$(dirname "$0")" ; pwd -P | sed 's/ /\\ /g' )"
sourcefile="$updaterpath/sources/$1"
echo $sourcefile
source $sourcefile
In the output I get the correct path echoed and the error from source saying it doesn't exists! The funny thing is that whether I cat that file, I can see the content, so the file path is correct!
/Users/cristian/Proyectos/MikroTik\ Updater/sources/testfile
/Users/cristian/Proyectos/MikroTik Updater/updater.sh: line 7: /Users/cristian/Proyectos/MikroTik\: No such file or directory
Your original question didn't include the faulty code:
### THIS IS BROKEN: the backslashes added by sed are literal, not syntactic
path=$(cd "$(dirname "$0")"; pwd -P | sed 's/ /\\ /g')
source $path/sources/$1
The sed is the source of your problem. Just get rid of it:
### THIS IS CORRECT: The syntactic quotes mean no backslashes are needed.
# ...also handles the case when the cd fails more gracefully.
path=$(cd "$(dirname "$0")" && pwd -P) || exit
source "$path/sources/$1"
...or, even better:
source "${BASH_SOURCE%/*}/sources/$1"
Backslashes are only meaningful when parsed as syntax. Results of string expansion do not go through these parsing steps. This is the same reason literal quotes can't be used to build a command in a string, as discussed in BashFAQ #50.
The code stayed this way if someone needs to see it
updaterpath="$( cd "$(dirname "$0")" ; pwd -P )"
sourcefile="$updaterpath/sources/$1"
echo $sourcefile
source "$sourcefile"

sed fails when "shopt -s nullglob" is set

Some days ago I started a little bash script that should sum up the number of pages and file size of all PDF's in a folder. It's working quite well now but there's still one thing I don't understand.
Why is sed always failing if shopt -s nullglob is set? Does somebody know why this happens?
I'm working with GNU Bash 4.3 and sed 4.2.2 in Ubuntu 14.04.
set -u
set -e
folder=$1
overallfilesize=0
overallpages=0
numberoffiles=0
#If glob fails nothing should be returned
shopt -s nullglob
for file in $folder/*.pdf
do
# Disable empty string if glob fails
# (Necessary because otherwise sed fails ?:|)
#shopt -u nullglob
# This command is allowed to fail
set +e
pdfinfo="$(pdfinfo "$file" 2> /dev/null)"
ret=$?
set -e
if [[ $ret -eq 0 ]]
then
#Remove every non digit in the result
sedstring='s/[^0-9]//g'
filesize=$(echo -e "$pdfinfo" | grep -m 1 "File size:" | sed $sedstring)
pages=$(echo -e "$pdfinfo" | grep -m 1 "Pages:" | sed $sedstring)
overallfilesize=$(($overallfilesize + $filesize))
overallpages=$(($overallpages+$pages))
numberoffiles=$(($numberoffiles+1))
fi
done
echo -e "Processed files: $numberoffiles"
echo -e "Pagesum: $overallpages"
echo -e "Filesizesum [Bytes]: $overallfilesize"
Here's a simpler test case for reproducing your problem:
#!/bin/bash
shopt -s nullglob
pattern='s/[^0-9]//g'
sed $pattern <<< foo42
Expected output:
42
Actual output:
Usage: sed [OPTION]... {script-only-if-no-other-script} [input-file]...
(sed usage follows)
This happens because s/[^0-9]//g is a valid glob (matching a dir structure like like s/c/g), and you asked bash to interpret it. Since you don't have a matching file, nullglob kicks in and removes the pattern entirely.
Double quoting prevents word splitting and glob interpretation, which is almost always what you want:
#!/bin/bash
shopt -s nullglob
pattern='s/[^0-9]//g'
sed "$pattern" <<< foo42
This produces the expected output.
You should always double quote all your variable references, unless you have a specific reason not to.

Deleting files by date in a shell script?

I have a directory with lots of files. I want to keep only the 6 newest. I guess I can look at their creation date and run rm on all those that are too old, but is the a better way for doing this? Maybe some linux command I could use?
Thanks!
:)
rm -v $(ls -t mysvc-*.log | tail -n +7)
ls -t, list sorted by time
tail -n +7, +7 here means length-7, so all but first 7 lines
$() makes a list of strings from the enclosed command output
rm to remove the files, of course
Beware files with space in their names, $() splits on any white-space!
Here's my take on it, as a script. It does handle spaces in file names even if it is a bit of a hack.
#!/bin/bash
eval set -- $(ls -t1 | sed -e 's/.*/"&"/')
if [[ $# -gt 6 ]] ; then
shift 6
while [[ $# -gt 0 ]] ; do
echo "remove this file: $1" # rm "$1"
shift
done
fi
The second option to ls up there is a "one" for one file name per line. Doesn't actually seem to matter, though, since that appears to be the default when ls isn't feeding a tty.

Get current directory or folder name (without the full path)

How could I retrieve the current working directory/folder name in a bash script, or even better, just a terminal command.
pwd gives the full path of the current working directory, e.g. /opt/local/bin but I only want bin.
No need for basename, and especially no need for a subshell running pwd (which adds an extra, and expensive, fork operation); the shell can do this internally using parameter expansion:
result=${PWD##*/} # to assign to a variable
result=${result:-/} # to correct for the case where PWD=/
printf '%s\n' "${PWD##*/}" # to print to stdout
# ...more robust than echo for unusual names
# (consider a directory named -e or -n)
printf '%q\n' "${PWD##*/}" # to print to stdout, quoted for use as shell input
# ...useful to make hidden characters readable.
Note that if you're applying this technique in other circumstances (not PWD, but some other variable holding a directory name), you might need to trim any trailing slashes. The below uses bash's extglob support to work even with multiple trailing slashes:
dirname=/path/to/somewhere//
shopt -s extglob # enable +(...) glob syntax
result=${dirname%%+(/)} # trim however many trailing slashes exist
result=${result##*/} # remove everything before the last / that still remains
result=${result:-/} # correct for dirname=/ case
printf '%s\n' "$result"
Alternatively, without extglob:
dirname="/path/to/somewhere//"
result="${dirname%"${dirname##*[!/]}"}" # extglob-free multi-trailing-/ trim
result="${result##*/}" # remove everything before the last /
result=${result:-/} # correct for dirname=/ case
Use the basename program. For your case:
% basename "$PWD"
bin
$ echo "${PWD##*/}"
​​​​​
Use:
basename "$PWD"
OR
IFS=/
var=($PWD)
echo ${var[-1]}
Turn the Internal Filename Separator (IFS) back to space.
IFS=
There is one space after the IFS.
You can use a combination of pwd and basename. E.g.
#!/bin/bash
CURRENT=`pwd`
BASENAME=`basename "$CURRENT"`
echo "$BASENAME"
exit;
How about grep:
pwd | grep -o '[^/]*$'
This thread is great! Here is one more flavor:
pwd | awk -F / '{print $NF}'
basename $(pwd)
or
echo "$(basename $(pwd))"
I like the selected answer (Charles Duffy), but be careful if you are in a symlinked dir and you want the name of the target dir. Unfortunately I don't think it can be done in a single parameter expansion expression, perhaps I'm mistaken. This should work:
target_PWD=$(readlink -f .)
echo ${target_PWD##*/}
To see this, an experiment:
cd foo
ln -s . bar
echo ${PWD##*/}
reports "bar"
DIRNAME
To show the leading directories of a path (without incurring a fork-exec of /usr/bin/dirname):
echo ${target_PWD%/*}
This will e.g. transform foo/bar/baz -> foo/bar
echo "$PWD" | sed 's!.*/!!'
If you are using Bourne shell or ${PWD##*/} is not available.
Surprisingly, no one mentioned this alternative that uses only built-in bash commands:
i="$IFS";IFS='/';set -f;p=($PWD);set +f;IFS="$i";echo "${p[-1]}"
As an added bonus you can easily obtain the name of the parent directory with:
[ "${#p[#]}" -gt 1 ] && echo "${p[-2]}"
These will work on Bash 4.3-alpha or newer.
There are a lots way of doing that I particularly liked Charles way because it avoid a new process, but before know this I solved it with awk
pwd | awk -F/ '{print $NF}'
For the find jockeys out there like me:
find $PWD -maxdepth 0 -printf "%f\n"
i usually use this in sh scripts
SCRIPTSRC=`readlink -f "$0" || echo "$0"`
RUN_PATH=`dirname "${SCRIPTSRC}" || echo .`
echo "Running from ${RUN_PATH}"
...
cd ${RUN_PATH}/subfolder
you can use this to automate things ...
Just use:
pwd | xargs basename
or
basename "`pwd`"
Below grep with regex is also working,
>pwd | grep -o "\w*-*$"
If you want to see only the current directory in the bash prompt region, you can edit .bashrc file in ~. Change \w to \W in the line:
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u#\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
Run source ~/.bashrc and it will only display the directory name in the prompt region.
Ref: https://superuser.com/questions/60555/show-only-current-directory-name-not-full-path-on-bash-prompt
I strongly prefer using gbasename, which is part of GNU coreutils.
Just run the following command line:
basename $(pwd)
If you want to copy that name:
basename $(pwd) | xclip -selection clipboard
An alternative to basname examples
pwd | grep -o "[^/]*$"
OR
pwd | ack -o "[^/]+$"
My shell did not come with the basename package and I tend to avoid downloading packages if there are ways around it.
You can use the basename utility which deletes any prefix ending in / and the suffix (if present in string) from string, and prints the
result on the standard output.
$basename <path-of-directory>
Just remove any character until a / (or \, if you're on Windows). As the match is gonna be made greedy it will remove everything until the last /:
pwd | sed 's/.*\///g'
In your case the result is as expected:
λ a='/opt/local/bin'
λ echo $a | sed 's/.*\///g'
bin
Here's a simple alias for it:
alias name='basename $( pwd )'
After putting that in your ~/.zshrc or ~/.bashrc file and sourcing it (ex: source ~/.zshrc), then you can simply run name to print out the current directories name.
The following commands will result in printing your current working directory in a bash script.
pushd .
CURRENT_DIR="`cd $1; pwd`"
popd
echo $CURRENT_DIR

Can I get the absolute path to the current script in KornShell?

Is it possible to find out the full path to the script that is currently executing in KornShell (ksh)?
i.e. if my script is in /opt/scripts/myscript.ksh, can I programmatically inside that script discover /opt/scripts/myscript.ksh ?
Thanks,
You could use:
## __SCRIPTNAME - name of the script without the path
##
typeset -r __SCRIPTNAME="${0##*/}"
## __SCRIPTDIR - path of the script (as entered by the user!)
##
__SCRIPTDIR="${0%/*}"
## __REAL_SCRIPTDIR - path of the script (real path, maybe a link)
##
__REAL_SCRIPTDIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
In korn shell, all of these $0 solutions fail if you are sourcing in the script in question. The correct way to get what you want is to use $_
$ cat bar
echo dollar under is $_
echo dollar zero is $0
$ ./bar
dollar under is ./bar
dollar zero is ./bar
$ . ./bar
dollar under is bar
dollar zero is -ksh
Notice the last line there? Use $_. At least in Korn. YMMV in bash, csh, et al..
Well it took me a while but this one is so simple it screams.
_SCRIPTDIR=$(cd $(dirname $0);echo $PWD)
since the CD operates in the spawned shell with $() it doesn't affect the current script.
How the script was called is stored in the variable $0. You can use readlink to get the absolute file name:
readlink -f "$0"
The variable $RPATH contains the relative path to the real file or the real path for a real file.
CURPATH=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
CURLOC=$CURPATH/`basename $0`
if [ `ls -dl $CURLOC |grep -c "^l" 2>/dev/null` -ne 0 ];then
ROFFSET=`ls -ld $CURLOC|cut -d ">" -f2 2>/dev/null`
RPATH=`ls -ld $CURLOC/$ROFFSET 2>/dev/null`
else
RPATH=$CURLOC
fi
echo $RPATH
This is what I did:
if [[ $0 != "/"* ]]; then
DIR=`pwd`/`dirname $0`
else
DIR=`dirname $0`
fi
readlink -f would be the best if it was portable, because it resolves every links found for both directories and files.
On mac os x there is no readlink -f (except maybe via macports), so you can only use readlink to get the destination of a specific symbolic link file.
The $(cd -P ... pwd -P) technique is nice but only works to resolve links for directories leading to the script, it doesn't work if the script itself is a symlink
Also, one case that wasn't mentioned : when you launch a script by passing it as an argument to a shell (/bin/sh /path/to/myscript.sh), $0 is not usable in this case
I took a look to mysql "binaries", many of them are actually shell scripts ; and now i understand why they ask for a --basedir option or need to be launched from a specific working directory ; this is because there is no good solution to locate the targeted script
This works also, although it won't give the "true" path if it's a link. It's simpler, but less exact.
SCRIPT_PATH="$(whence ${0})"
Try which command.
which scriptname
will give you the full qualified name of the script along with its absolute path
I upgraded the Edward Staudt's answer, to be able to deal with absolute-path symbolic links, and with chains of links too.
DZERO=$0
while true; do
echo "Trying to find real dir for script $DZERO"
CPATH=$( cd -P -- "$(dirname -- "$(command -v -- "$DZERO")")" && pwd -P )
CFILE=$CPATH/`basename $DZERO`
if [ `ls -dl $CFILE | grep -c "^l" 2>/dev/null` -eq 0 ];then
break
fi
LNKTO=`ls -ld $CFILE | cut -d ">" -f2 | tr -d " " 2>/dev/null`
DZERO=`cd $CPATH ; command -v $LNKTO`
done
Ugly, but works...
After run this, the path is $CPATH and the file is $CFILE
Try using this:
dir = $(dirname $0)
Using $_ provides the last command.
>source my_script
Works if I issue the command twice:
>source my_script
>source my_script
If I use a different sequence of commands:
>who
>source my_script
The $_ variable returns "who"

Resources