1

In Bash, v.4.3.11(1) I have this sample code:

#!/bin/bash

dir=("/home/user/Documents/" "/home/user/Music/" "/home/user/Videos/" \
"/home/user/Photos/")

baseDir="/home/user/"

for i in "${!dir[@]}"
do
    niceName=${dir[$i]#"$baseDir"}  # removes baseDir from front part
    printf "%s\n" "${niceName%"/"}"  # also removes trailing slash from end
done

Is there a way to combine the two commands in one and have only the printf within the for loop? (preferably without resorting to awk or sed, but ok, if inevitable).

I have tried various combinations but I am ending up with "bad substitution" errors. For example, printf "%s\n" "${niceName=${dir[$i]#"$baseDir"%"/"}" is not working for me.

knedas
  • 178
  • 2
  • 13
  • 3
    Is there any reason you're using `for i in "${!dir[@]}"` and `${dir[$i]}` instead of `for i in "${dir[@]}"` and just `$i`? – Benjamin W. Jan 19 '17 at 23:45
  • 1
    You don't need the backslash in the definition of the array; newlines work just as well as spaces to separate array elements. – chepner Jan 20 '17 at 04:28
  • Benjamin W.: needed to have the index easily available, but you're right in the general case. Chepner: useful observation, thank you! – knedas Jan 20 '17 at 09:03

4 Answers4

3

Here is a simpler version.

#!/bin/bash
dir=("/home/user/Documents/" "/home/user/Music/" "/home/user/Videos/" "/home/user/Photos/")

for d in "${dir[@]}"
do
  basename "$d"
done

Please note the argument to basename needs to be quoted, or else directory names with some characters (such as spaces) will cause problems.

The inside of the loop could also be replaced by the builtins-based solutions below (faster and without external dependencies) :

  d="${d%/}"
  echo "${d##*/}"
Fred
  • 6,590
  • 9
  • 20
  • `basename` is substantially inefficient compared to a similar parameter expansion. `${d##*/}` is much, *much* faster to evaluate because it's built into the shell, whereas `basename` is a separate executable. – Charles Duffy Jan 20 '17 at 00:53
  • 1
    It executes much faster, and would sure be the right choice in a context where speed matters or for someone knowledgeable who writes this from memory. In that sense it is better. It is more difficult to remember though, so for most people the solution easiest to remember might be quicker once you inclure the time they need to look it up. – Fred Jan 20 '17 at 00:59
  • If the fact that one already remembers it is sufficient cause to retain a bad habit, that habit will never be dislodged. Performance is only one aspect -- another is robustness: Shell builtins won't fail if an invalid PATH, bad LD_LIBRARY_PATH, or risk being missing if running on an embedded platform that's been stripped to its bones. Much of my beef is that shell scripting has a reputation of being innately slow that owes more to commonly accepted idiom and practices than technical limitations; if you look at the justification for systemd, "shell scripts are inefficient" was a big part. – Charles Duffy Jan 20 '17 at 01:04
  • ...now, bash *is* an inefficient interpreter, but not all interpreters are: ksh93 is very fast when running a well-designed script (that doesn't require subshells or external commands). But if nobody ever *writes* scripts that follow good practices, then it doesn't matter how efficient a given interpreter is: A script that innately can't be efficiently interpreted is just that, and the more ubiquitous those are, the more we see shell scripting getting an even worse reputation than it deserves. – Charles Duffy Jan 20 '17 at 01:05
  • So -- I'm out here doing my part to encourage folks to learn idioms that are robust and efficient (and secure -- practices that compromise robustness very frequently have security implications as well), and quite confident that in doing so I'm in some small way making the world a better place. :) – Charles Duffy Jan 20 '17 at 01:08
  • I would say that, in very many cases, it is in fact the case that interpreter efficiency does not matter, in the sense that for what a given user or admin needs to do, Bash is plenty fast enough despite being very slow relative to other shells, or C, or assembly... But I understand what you are saying, strictly speaking you are absolutely right. – Fred Jan 20 '17 at 01:20
  • Thanks for the comments. I appreciate a lot seeing well thought-out solutions I had never seen before and make me reconsider how I currently do things, especially when I have an explanation by someone experienced on why it is the right way to do it. Thanks a lot for helping us. – Fred Jan 20 '17 at 01:24
  • Ooh. Looking at the answer as amended in the context of the OP's input, there's actually a place here where there's a bug in some of my advice: Since you have names ending with a `/`, `${d##*/}` (deleting everything up to and including the last `/` in a string) only operates as a proper equivalent to `basename` when preceded by `d=${d%/}` (trimming the trailing character from a string, when that character is a `/`). I'm most sincerely embarrassed. – Charles Duffy Jan 20 '17 at 02:56
  • Fred's second suggestion is roughly what I do in the question's code. Isn't it? From all the comments, my understanding is: 1. basename works, but built-ins (which I also used in my original example) are both faster and recommended. 2. basename would be irrelevant if the original array did not contain dirs, so built-ins also cover the general case. 3. Double-parameter expansion in bash on one line does not work. Right? – knedas Jan 20 '17 at 09:17
  • My suggestion is simpler in that it avoids accessing each array element with its index, and avoids having to use the baseDir variable, but yes, the logic is similar. 1. basename is built to handle paths (and an external command), so it does the job with one fewer (slower) statement. 2. If you are comfortable with builtins, they are the way to go. 3. If you want to put both statements on one line you can, separating them with a semi-colon, but that is just changing spacing, the syntax stays the same. – Fred Jan 20 '17 at 11:27
2

If you're looking for a one-liner using substitution, this'll work:

dir=("/home/user/Documents/" "/home/user/Music/" "/home/user/Videos/" \
"/home/user/Photos/")
baseDir="/home/user/"
dir=("${dir[@]%/}")    ## This line removes trailing forward slashes
printf "%s\n" "${dir[@]#$baseDir}"
Jeffrey Cash
  • 1,023
  • 6
  • 12
  • Incidentally, the OP's use of `printf '%s\n'` instead of `echo` is probably a good call here -- it distinguishes between `foo bar` and `"foo bar"`. This answer *is* correct, but one can't unambiguously determine that from echo's output, because the output is indistinguishable between `"${dir[*]}"`-style and `"${dir[@]}"`-style expansion cases. – Charles Duffy Jan 20 '17 at 00:51
  • You are right! I overlooked that and have edited it to behave properly and put each on its own line. Also, if for some reason you wanted it to work with echo, you could get the same functionality by using `(IFS=$'\n'; echo "${dir[*]#$baseDir}")` to print them. – Jeffrey Cash Jan 20 '17 at 01:12
  • Your code does not remove the trailing slash for me. – knedas Jan 20 '17 at 08:39
  • @knedas I have now updated it to remove the trailing slashes – Jeffrey Cash Jan 20 '17 at 15:11
1

You could use basename which returns just the base file name. In this case, your file is the directory.

#!/bin/bash
dir=("/home/user/Documents/" "/home/user/Music/" "/home/user/Videos/" "/home/user/Photos/")

for i in "${!dir[@]}"
do
  echo $( basename ${dir[$i]})
done
  • 3
    Using command substitution $( ) to capture the output of a command and "echo" on this output is redundant. "basename" already echoes its output. Also, the argument to basename needs to be quoted. – Fred Jan 19 '17 at 23:58
  • +1 for suggesting `basename` but Fred is right about the corrections and also this does not address the general issue of double parameter expansion because it works only with dirs. – knedas Jan 20 '17 at 08:43
1

As far as I know bash does not handle "double parameter expansion" (I believe zsh does). However, you can hack together a solution like this:

dir=("/home/user/Documents/" "/home/user/Music/" "/home/user/Videos/" "/home/user/Photos/")
trunk=/home/user/

echo $(dir=( "${dir[@]##${trunk}}" ); echo "${dir[@]%/}")
Sigve Karolius
  • 1,356
  • 10
  • 26
  • Quotes are important -- `dir=( ${foo[@]} )` is not the same as `dir=( "${foo[@]}" )` -- play around with names having spaces. – Charles Duffy Jan 20 '17 at 00:47
  • ...incidentally, "names having spaces" (or newlines, or potentially literally any other non-NUL character) are why this approach is unsafe to use: The only way you can safely keep your array elements separate from each other that works with all possible filenames is to NUL-delimit them, but NULs in a command substitution are silently deleted. – Charles Duffy Jan 20 '17 at 00:49
  • (Note that "all possible filenames" even includes newlines, so you can't trust those either). – Charles Duffy Jan 20 '17 at 00:50
  • Modified it to `printf "%s\n" $(dir=( "${dir[$i]##${baseDir}}" ); echo "${dir[@]%/}"` to fit the example (one entry per line), but does not work as intended when dirs have spaces. However, thank you for letting me know that bash does not handle double parameter expansion. This is the underlying issue in my question. – knedas Jan 20 '17 at 08:34