28

I am trying out shellcheck.

I have something like that

basename "${OPENSSL}" 

and I get the following suggestion

Use parameter expansion instead, such as ${var##*/}.

From the practical point of view I see no difference

$ export OPENSSL=/opt/local/bin/openssl
$ basename ${OPENSSL}
openssl
$ echo ${OPENSSL##*/}
openssl

Since basename is in the POSIX specs, I don't a reason why it should be best practice. Any hint?

9
  • 9
    It forks a new process when it doesn't need to.
    – jordanm
    Oct 9, 2013 at 16:08
  • @jordanm fair enough ... I didn't think about efficiency.
    – Matteo
    Oct 9, 2013 at 16:09
  • 3
    @jordanm On the other hand it works with on shells other than bash
    – Matteo
    Oct 9, 2013 at 16:11
  • 1
    @JosephR. that's what I thought but just found out that it does not work on csh. I guess csh is not POSIX then.
    – terdon
    Oct 9, 2013 at 16:20
  • 3
    @terdon - csh is very far from POSIX.
    – jordanm
    Oct 9, 2013 at 16:29

3 Answers 3

28

It's not about efficiency -- it's about correctness. basename uses newlines to delimit the filenames it prints out. In the usual case when you only pass one filename, it adds a trailing newline to its output. Since filenames may contain newlines themselves, this makes it difficult to correctly handle these filenames.

It's further complicated by the fact that people usually use basename like this: "$(basename "$file")". This makes things even more difficult, because $(command) strips all trailing newlines from command. Consider the unlikely case that $file ends with a newline. Then basename will add an extra newline, but "$(basename "$file")" will strip both newlines, leaving you with an incorrect filename.

Another problem with basename is that if $file begins with a - (dash a.k.a. minus), it will be interpreted as an option. This one is easy to fix: $(basename -- "$file")

The robust way of using basename is this:

# A file with three trailing newlines.
file=$'/tmp/evil\n\n\n'

# Add an 'x' so we can tell where $file's newlines end and basename's begin.
file_x="$(basename -- "$file"; printf x)"

# Strip off two trailing characters: the 'x' added by us and the newline added by basename. 
base="${file_x%??}"

An alternative is to use ${file##*/}, which is easier but has bugs of its own. In particular, it's wrong in the cases where $file is / or foo/.

4
  • 2
    Very good points, +1. Glad the OP accepted this instead of mine.
    – terdon
    Oct 9, 2013 at 19:50
  • 2
    Counterpoint: what if $file is foo/? What if it's /? Oct 9, 2013 at 22:17
  • 2
    @Gilles: You're right. I'm starting to think that the basename approach is better after all, as hacky as it is. The best alternatives I can come up with are ${${${file}%%/#}##*/} and [[ $file =~ "([^/]*)/*$" ]] && printf "%s" $match[1], both of which are zsh-specific and neither of which handle / correctly.
    – Matt
    Oct 10, 2013 at 2:27
  • @terdon Thanks that you didn't take it personally :-). Files with newlines are not common but Matt has a point. Of course using variable substitution is also more efficient.
    – Matteo
    Oct 10, 2013 at 6:27
17

The relevant lines in shellcheck's source code are:

checkNeedlessCommands (T_SimpleCommand id _ (w:_)) | w `isCommand` "dirname" =
    style id "Use parameter expansion instead, such as ${var%/*}."
checkNeedlessCommands (T_SimpleCommand id _ (w:_)) | w `isCommand` "basename" =
    style id "Use parameter expansion instead, such as ${var##*/}."
checkNeedlessCommands _ = return ()

There is no explanation given explicitly but based on the name of the function (checkNeedlessCommands) it looks like @jordanm is quite right and it is suggesting you avoid forking a new process.

1
  • 7
    May the source be with you :)
    – Joseph R.
    Oct 9, 2013 at 16:29
3

dirname, basename, readlink etc (thanks @Marco - this is corrected) can create portability problems when security becomes important (requiring security of the path). Many systems (like Fedora Linux) place it at /bin whereas others (like Mac OSX) place it at /usr/bin. Then there is Bash on Windows, eg cygwin, msys, and others. It's always better to stay pure Bash, when possible. (per @Marco comment)

BTW, thanks for the pointer to shellcheck, I haven't seen that before.

6
  • 2
    1) What do you mean by “security”? Can you elaborate? 2) The OP does not mention dirname at all. 3) Basic core utilities should be in the PATH, wherever they are stored. I haven't yet seen a system where basename was not in the PATH. 4) Assuming bash is available is a portibility issue. It's always better to stay away from bash and use a POSIX shell when portability is required.
    – Marco
    Oct 9, 2013 at 23:03
  • @Marco Thanks for pointing out these issues. Yes you are correct that the utilities are on the path. But where one wants to provide added security to a Bash script, it's good practice to provide the absolute path. So calling '/bin/basename' will work for RedHat systems, but it will produce an error on a Mac. This is better explained in the Bash Cookbook where about one quarter of the 600 pages is devoted to this subject. We have made a rough attempt to address the security issues there in our free source secure-lib.
    – AsymLabs
    Oct 10, 2013 at 6:31
  • @Marco Point 4 is a valid comment but the question (OP) begins with and is written around shellcheck which is designed to check both sh/bash scripts. Therefore we have to assume it is not strictly Posix by default.
    – AsymLabs
    Oct 10, 2013 at 6:42
  • 1
    The OP specifically mentions POSIX and the question is tagged posix and not bash. I fail to find any indicator that the OP requires a bash solution. Your statement “It's always better to stay pure Bash” is just plain wrong, I'm sorry.
    – Marco
    Oct 10, 2013 at 8:09
  • 1
    @ Marco Point well made and accepted in the context of this thread. It could also be argued that shellcheck was not the correct tool in this case.
    – AsymLabs
    Oct 10, 2013 at 8:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.