97

What I have is this:

progname=${0%.*}
progname=${progname##*/}

Can this be nested (or not) into one line, i.e. a single expression?

I'm trying to strip the path and extension off of a script name so that only the base name is left. The above two lines work fine. My 'C' nature is simply driving me to obfuscate these even more.

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
user71918
  • 1,218
  • 1
  • 9
  • 7

15 Answers15

81

Bash supports indirect expansion:

$ FOO_BAR="foobar"
$ foo=FOO
$ foobar=${foo}_BAR
$ echo ${foobar}
FOO_BAR
$ echo ${!foobar}
foobar

This should support the nesting you are looking for.

Sparhawk
  • 1,581
  • 1
  • 19
  • 29
user1956358
  • 811
  • 6
  • 2
  • 8
    Hi what does the ! means in shell? It means replace with foobar's value here? – Mike Mar 27 '14 at 20:52
  • Great example. What would one do in this case if they didn't have the indirection parameter expansion available? – Nadim Hussami Apr 14 '17 at 09:57
  • This worked great, thanks! :) Unfortunately I made a minor typo that made me question that fact and google things for an hour, haha. – John Humphreys Sep 12 '20 at 23:48
  • 2
    @Mike `${var}` is the value stored in the variable named `var` but `${!var}` is the value stored in the variable who's name is stored in `${var}`. It's a form of indirection. – Paul Evans Mar 23 '22 at 00:01
71

If by nest, you mean something like this:

#!/bin/bash

export HELLO="HELLO"
export HELLOWORLD="Hello, world!"

echo ${${HELLO}WORLD}

Then no, you can't nest ${var} expressions. The bash syntax expander won't understand it.

However, if I understand your problem right, you might look at using the basename command - it strips the path from a given filename, and if given the extension, will strip that also. For example, running basename /some/path/to/script.sh .sh will return script.

Tim
  • 59,527
  • 19
  • 156
  • 165
  • If you're using indices though, they can be, correct? e.g., `ARR=('foo' 'bar' 'bogus'); i=0; while /bin/true ; do echo ${ARR[$i]} ; i=(( (i + 1) % 3 )); done` which is obviously useless as code but works as an example. – Ian Stapleton Cordasco Jan 07 '13 at 21:58
  • 2
    Actually, this *is* supported in certain cases, at least in `bash` and `ksh`. This works: `x=yyy; y=xxxyyy; echo ${y%${x}}`. I think the important bit is that the nested expansion is an argument of one of the operators. I've not seen that it's documented really well, though. – twalberg Jan 07 '13 at 22:07
  • 1
    This isn't working for me, I get bash: ${${HELLO}WORLD}: bad substitution. Any Ideas? I'm on fedora. – Carlos Bribiescas Jan 20 '16 at 20:27
  • 2
    @CarlosBribiescas, as @Tim said, "no, you can't nest expressions". The example `echo ${${HELLO}WORLD}` illustrates *this* impossibility. – Atcold Feb 27 '17 at 20:25
  • Instead of *indirect expansion*, you could use *nameref* for building variable name! [See my answer](https://stackoverflow.com/a/61364880/1765658) – F. Hauri - Give Up GitHub Mar 06 '21 at 15:36
  • Can the expansion occur on the LHS like it does in Makefiles? – gone Jul 30 '21 at 15:46
23

The following option has worked for me:

NAME="par1-par2-par3"
echo $(TMP=${NAME%-*};echo ${TMP##*-})

Output is:

par2
Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
luferbraho
  • 247
  • 2
  • 2
21

An old thread but perhaps the answer is the use of Indirection:${!PARAMETER}

For e.g., consider the following lines:

H="abc"
PARAM="H"
echo ${!PARAM} #gives abc
Deepak
  • 3,648
  • 1
  • 22
  • 17
13

This nesting does not appear to be possible in bash, but it works in zsh:

progname=${${0%.*}##*/}
Nathan Kitchen
  • 4,799
  • 1
  • 24
  • 19
9

Expressions like ${${a}} do not work. To work around it, you can use eval:

b=value
a=b
eval aval=\$$a
echo $aval

Output is

value

Gavin Smith
  • 3,076
  • 1
  • 19
  • 25
8

Actually it is possible to create nested variables in bash, using two steps.

Here is a test script based upon the post by Tim, using the idea suggested by user1956358.

#!/bin/bash
export HELLO="HELLO"
export HELLOWORLD="Hello, world!"

# This command does not work properly in bash
echo ${${HELLO}WORLD}

# However, a two-step process does work
export TEMP=${HELLO}WORLD
echo ${!TEMP}

The output is:

Hello, world!

There are lots of neat tricks explained by running 'info bash' from the command line, then searching for 'Shell Parameter Expansion'. I've been reading a few myself today, just lost about 20 minutes of my day, but my scripts are going to get a lot better...

Update: After more reading I suggest this alternative per your initial question.

progname=${0##*/}

It returns

bash
jmw86069
  • 134
  • 1
  • 4
  • 1
    The OP asked if nested expansion is possible in bash, and basically you answer (which is correct): No, you need to have to expand expression one into a temporary variable and then use the second expression on that variable. Pattern nesting is, however, supported in other shells. – Christian Herenz Jun 05 '18 at 12:36
5

There is a 1 line solution to the OP's original question, the basename of a script with the file extension stripped:

progname=$(tmp=${0%.*} ; echo ${tmp##*/})

Here's another, but, using a cheat for basename:

progname=$(basename ${0%.*})

Other answers have wandered away from the OP's original question and focused on whether it's possible to just expand the result of expressions with ${!var} but came across the limitation that var must explicitly match an variable name. Having said that, there's nothing stopping you having a 1-liner answer if you chain the expressions together with a semicolon.

ANIMAL=CAT
BABYCAT=KITTEN
tmp=BABY${ANIMAL} ; ANSWER=${!tmp} # ANSWER=KITTEN

If you want to make this appear like a single statement, you can nest it in a subshell, i.e.

ANSWER=$( tmp=BABY${ANIMAL) ; echo ${!tmp} ) # ANSWER=KITTEN

An interesting usage is indirection works on arguments of a bash function. Then, you can nest your bash function calls to achieve multilevel nested indirection because we are allowed to do nested commands:

Here's a demonstration of indirection of an expression:

deref() { echo ${!1} ; }
ANIMAL=CAT
BABYCAT=KITTEN
deref BABY${ANIMAL} # Outputs: KITTEN

Here's a demonstration of multi level indirection thru nested commands:

deref() { echo ${!1} ; }
export AA=BB
export BB=CC
export CC=Hiya
deref AA # Outputs: BB
deref $(deref AA) # Outputs: CC
deref $(deref $(deref AA)) # Outputs: Hiya
Stephen Quan
  • 21,481
  • 4
  • 88
  • 75
  • Thanks for the indirection explanation! It is indeed a bit unfortunate that it can only be used to reference existing variable names. Of course a oneliner can be made but involving the assignment of a temporary variable is what I was trying to avoid, at the end of the day. – Steven Lu Feb 06 '18 at 23:14
  • @steven-lu what about my `deref()` function? If you have that declared, you can have your oneliner without a temporary variable? – Stephen Quan Feb 06 '18 at 23:48
  • By any reasonable measure, that’s like twice to three times more confusing for maintenance than use of a temp variable reminds me of C preprocessor abuse – Steven Lu Feb 07 '18 at 16:52
4

As there is already a lot of answer there, I just want to present two different ways for doing both: nesting parameter expansion and variable name manipulation. (So you will find four different answer there:).

Parameter expansion not really nested, but done in one line:

Without semicolon (;) nor newline:

progname=${0%.*} progname=${progname##*/}

Another way: you could use a fork to basename

progname=$(basename ${0%.*})

This will make the job.

About concatenating variable name

If you want to construct varname, you could

use indirect expansion

foobar="baz"
varname="foo"
varname+="bar"
echo ${!varname}
baz

or use nameref

foobar="baz"
bar="foo"
declare -n reffoobar=${bar}bar
echo $reffoobar
baz
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • Wow. I can see nameref getting me into a lot of trouble. Just because it's there, should I use it? And will I remember such obscurity in a week's time? – Gary Dean Jan 03 '22 at 00:38
2

I know this is an ancient thread, but here are my 2 cents.

Here's an (admittedly kludgy) bash function which allows for the required functionality:

read_var() {
  set | grep ^$1\\b | sed s/^$1=//
}

Here's a short test script:

#!/bin/bash

read_var() {
  set | grep ^$1\\b | sed s/^$1=//
}

FOO=12
BAR=34

ABC_VAR=FOO
DEF_VAR=BAR

for a in ABC DEF; do
  echo $a = $(read_var $(read_var ${a}_VAR))
done

The output is, as expected:

ABC = 12
DEF = 34
diegog
  • 51
  • 3
  • I know this is very old now, but you could do away with the grep and use sed -n p as: `set | sed -n s/^$1=//p` . BTW nice hack. – Dani_l Mar 18 '19 at 09:47
2

It will work if you follow the bellow shown way of taking on intermediate step :

export HELLO="HELLO"
export HELLOWORLD="Hello, world!"

varname=${HELLO}WORLD
echo ${!varname}
Prashant
  • 21
  • 1
1

Though this is a very old thread, this device is ideal for either directly or randomly selecting a file/directory for processing (playing tunes, picking a film to watch or book to read, etc).

In bash I believe it is generally true that you cannot directly nest any two expansions of the same type, but if you can separate them with some different kind of expansion, it can be done.

e=($(find . -maxdepth 1 -type d))
c=${2:-${e[$((RANDOM%${#e[@]}))]}}

Explanation: e is an array of directory names, c the selected directory, either named explicitly as $2,

${2:-...}  

where ... is the alternative random selection given by

${e[$((RANDOM%${#e[@]}))]}  

where the

$((RANDOM%...))  

number generated by bash is divided by the number of items in array e, given by

${#e[@]}  

yielding the remainder (from the % operator) that becomes the index to array e

${e[...]}

Thus you have four nested expansions.

user985675
  • 293
  • 3
  • 10
1

The basename bultin could help with this, since you're specifically splitting on / in one part:

user@host# var=/path/to/file.extension
user@host# basename ${var%%.*}
file
user@host#

It's not really faster than the two line variant, but it is just one line using built-in functionality. Or, use zsh/ksh which can do the pattern nesting thing. :)

dannysauer
  • 3,793
  • 1
  • 23
  • 30
0

If the motivation is to "obfuscate" (I would say streamline) array processing in the spirit of Python's "comprehensions", create a helper function that performs the operations in sequence.

function fixupnames()
   {
   pre=$1 ; suf=$2 ; shift ; shift ; args=($@)
   args=(${args[@]/#/${pre}-})
   args=(${args[@]/%/-${suf}})
   echo ${args[@]}
   }

You can use the result with a nice one-liner.

$ echo $(fixupnames a b abc def ghi)
a-abc-b a-def-b a-ghi-b
Brent Bradburn
  • 51,587
  • 17
  • 154
  • 173
-1

eval will allow you to do what you are wanting:

export HELLO="HELLO"
export HELLOWORLD="Hello, world!"

eval echo "\${${HELLO}WORLD}"

Output: Hello, world

jgshawkey
  • 2,034
  • 1
  • 9
  • 8