Related
Let's say I have these variables:
VAR_A=resultA
VAR_B=resultB
X=A
I want to get the value of VAR_A or VAR_B based on the value of X.
This is working and gives resultA:
VAR="VAR_$X"
RESULT=${!VAR}
My question is, is there a one-liner for this?
Because indirection expansion doesn't seem to work if it is not the wole name of the variable which is expanded. I tried:
RESULT=${!VAR_$X}
RESULT=${!"VAR_$X"}
...and a lot of other combinations, but it always write "bad substitution"...
There doesn't appear to be a shorter way when using the bash 2 notation of ${!var}.
For reference, the "new" bash 2 notation is value=${!string_name_var} and the "old" notation would be: eval value=\$$string_name_var.
You could just use the old notation to make it work like you wanted:
eval RESULT=\$"VAR_$X"
Reference: https://www.tldp.org/LDP/abs/html/bashver2.html#BASH2REF
You'd be better off using an associative array, at least on recent versions of Bash:
declare -A var=(
[A]=resultA
[B]=resultB
)
x=A
result="${var[$x]}"
echo "$result" # -> resultA
This should work.
RESULT=$(eval echo \$VAR_$X)
The \ in front of the $ makes it be treated literally.
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.
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.
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.
The following option has worked for me:
NAME="par1-par2-par3"
echo $(TMP=${NAME%-*};echo ${TMP##*-})
Output is:
par2
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
This nesting does not appear to be possible in bash, but it works in zsh:
progname=${${0%.*}##*/}
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
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
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
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
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
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}
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. :)
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.
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
eval will allow you to do what you are wanting:
export HELLO="HELLO"
export HELLOWORLD="Hello, world!"
eval echo "\${${HELLO}WORLD}"
Output: Hello, world
I have a script which uses the following logic:
if [ ! -z "$1" ]; then # if any parameter is supplied
ACTION= # clear $ACTION
else
ACTION=echo # otherwise, set it to 'echo'
fi
This works fine, as-is. However, in reading the Shell Parameter Expansion section of the bash manual, it seems this should be able to be done in a single step. However, I can't quite wrap my head around how to do it.
I've tried:
ACTION=${1:-echo} # ends up with $1 in $ACTION
ACTION=${1:+}
ACTION=${ACTION:-echo} # ends up always 'echo'
and a few ways of nesting them, but nesting seems to be disallowed as far as I can tell.
I realize I've already got a working solution, but now I'm genuinely curious if this is possible. It's something that would be straightforward with a ternary operator, but I don't think bash has one.
If this is possible, I'd like to see the logic to do this seeming two-step process, with no if/else constructs, but using only any combination of the Shell Parameter Expansion features.
Thank you.
EDIT for elderarthis:
The remainder of the script is just:
find . -name "*\?[NMSD]=[AD]" -exec ${ACTION} rm -f "{}" +
I just want ACTION=echo as a sanity check against myself, hence, passing any argument will actually do the deletion (by nullifying ${ACTION}, whereas passing no args leaves echo in there.
And I know TIMTOWTDI; I'm looking to see if it can be done with just the stuff in the Shell Parameter Expansion section :-)
EDIT for Mikel:
$ cat honk.sh
#!/bin/bash
ACTION=${1-echo}
echo $ACTION
$ ./honk.sh
echo
$ ./honk.sh foo
foo
The last needs to have ACTION='', and thus return a blank line/null value.
If I insisted on doing it in fewer than 4 lines and no sub-shell, then I think I'd use:
ACTION=${1:+' '}
: ${ACTION:=echo}
This cheats slightly - it creates a blank action rather than an empty action if there is an argument to the script. If there is no argument, then ACTION is empty before the second line. On the second line, if action is empty, set it to 'echo'. In the expansion, since you (correctly) do not quote $ACTION, no argument will be passed for the blank.
Tester (xx.sh):
ACTION=${1:+' '}
: ${ACTION:=echo}
echo $ACTION rm -f a b c
Tests:
$ sh xx.sh 1
rm -f a b c
$ sh xx.sh
echo rm -f a b c
$ sh xx.sh ''
echo rm -f a b c
$
If the last line is incorrect, then remove the colon from before the plus.
If a sub-shell is acceptable, then one of these two single lines works:
ACTION=$([ -z "$1" ] && echo echo)
ACTION=$([ -z "${1+X}" ] && echo echo)
The first corresponds to the first version shown above (empty first arguments are treated as absent); the second deals with empty arguments as present. You could write:
ACTION=$([ -z "${1:+X}" ] && echo echo)
to make the relation with the second clearer - except you're only going to use one or the other, not both.
Since the markdown notation in my comment confused the system (or I got it wrong but didn't get to fix it quickly enough), my last comment (slightly amended) should read:
The notation ${var:+' '} means 'if $var is set and is not empty, then use what follows the +' (which, in this case, is a single blank). The notation ${var+' '} means 'if $var is set - regardless of whether it is empty or not - then use what follows the +'. These other expansions are similar:
${var:=X} - set $var to X unless it already has a non-empty value.
${var:-X} - expands to $var if it has a non-empty value and expands to X if $var is unset or is empty
Dropping the colon removes the 'empty' part of the test.
ACTION=${1:-echo}
is correct.
Make sure it's near the top of your script before anything modifies $1 (e.g. before any set command). Also, it wouldn't work inside a function, because $1 would be the first parameter to the function.
Also check if $1 is set but null, in which case fix how you're calling it, or use ACTION=${1-echo} (note there is no :).
Update
Ah, I assumed you must have meant the opposite, because it didn't really make sense otherwise.
It still seems odd, but I guess as a mental exercise, maybe you want something like this:
#!/bin/bash
shopt -s extglob
ACTION=$1
ACTION=${ACTION:-echo}
ACTION=${ACTION/!(echo)/} # or maybe ACTION=${ACTION#!(echo)}
echo ACTION=$ACTION
It's not quite right: it gives ACTION=o, but I think something along those lines should work.
Further, if you pass echo as $1, it will stay as echo, but I don't think that's a bad thing.
It's also terribly ugly, but you knew that when asking the question. :-)
I'm not entirely new to programming, but I'm not exactly experienced. I want to write small shell script for practice.
Here's what I have so far:
#!/bin/sh
name=$0
links=$3
owner=$4
if [ $# -ne 1 ]
then
echo "Usage: $0 <directory>"
exit 1
fi
if [ ! -e $1 ]
then
echo "$1 not found"
exit 1
elif [ -d $1 ]
then
echo "Name\t\tLinks\t\tOwner\t\tDate"
echo "$name\t$links\t$owner\t$date"
exit 0
fi
Basically what I'm trying to do is have the script go through all of the files in a specified directory and then display the name of each file with the amount of links it has, its owner, and the date it was created. What would be the syntax for displaying the date of creation or at least the date of last modification of the file?
Another thing is, what is the syntax for creating a for loop? From what I understand I would have to write something like for $1 in $1 ($1 being all of the files in the directory the user typed in correct?) and then go through checking each file and displaying the information for each one. How would I start and end the for loop (what is the syntax for this?).
As you can see I'm not very familiar bourne shell programming. If you have any helpful websites or have a better way of approaching this please show me!
Syntax for a for loop:
for var in list
do
echo $var
done
for example:
for var in *
do
echo $var
done
What you might want to consider however is something like this:
ls -l | while read perms links owner group size date1 date2 time filename
do
echo $filename
done
which splits the output of ls -l into fields on-the-fly so you don't need to do any splitting yourself.
The field-splitting is controlled by the shell-variable IFS, which by default contains a space, tab and newline. If you change this in a shell script, remember to change it back. Thus by changing the value of IFS you can, for example, parse CSV files by setting this to a comma. this example reads three fields from a CSV and spits out the 2nd and 3rd only (it's effectively the shell equivalent of cut -d, -f2,3 inputfile.csv)
oldifs=$IFS
IFS=","
while read field1 field2 field3
do
echo $field2 $field3
done < inputfile.csv
IFS=oldifs
(note: you don't need to revert IFS, but I generally do to make sure that further text processing in a script isn't affected after I'm done with it).
Plenty of documentation out the on both for and while loops; just google for it :-)
$1 is the first positional parameter, so $3 is the third and $4 is the fourth. They have nothing to do with the directory (or its files) the script was started from. If your script was started using this, for example:
./script.sh apple banana cherry date elderberry
then the variable $1 would equal "apple" and so on. The special parameter $# is the count of positional parameters, which in this case would be five.
The name of the script is contained in $0 and $* and $# are arrays that contain all the positional parameters which behave differently depending on whether they appear in quotes.
You can refer to the positional parameters using a substring-style index:
${#:2:1}
would give "banana" using the example above. And:
${#: -1}
or
${#:$#}
would give the last ("elderberry"). Note that the space before the minus sign is required in this context.
You might want to look at Advanced Bash-Scripting Guide. It has a section that explains loops.
I suggest to use find with the option -printf "%P\t%n\t%u\t%t"
for x in "$#"; do
echo "$x"
done
The "$#" protects any whitespace in supplied file names. Obviously, do your real work in place of "echo $x", which isn't doing much. But $# is all the junk supplied on the command line to your script.
But also, your script bails out if $# is not equal to 1, but you're apparently fully expecting up to 4 arguments (hence the $4 you reference in the early part of your script).
assuming you have GNU find on your system
find /path -type f -printf "filename: %f | hardlinks: %n| owner: %u | time: %TH %Tb %TY\n"
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.
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.
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.
The following option has worked for me:
NAME="par1-par2-par3"
echo $(TMP=${NAME%-*};echo ${TMP##*-})
Output is:
par2
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
This nesting does not appear to be possible in bash, but it works in zsh:
progname=${${0%.*}##*/}
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
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
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
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
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
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}
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. :)
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.
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
eval will allow you to do what you are wanting:
export HELLO="HELLO"
export HELLOWORLD="Hello, world!"
eval echo "\${${HELLO}WORLD}"
Output: Hello, world