I'm looking at some old scripts and I found some parameter assignment that I have not seen before. A while loop reads from a text file and passes the values to a function. The items in the text file look like this:
user_one:abcdef:secretfolder
the first stage of the function then looks like this:
IFS=':' read -a param <<< $#
user="${param[0]}"
pass="${param[1]}"
user_folders="${param[2]}"
I have not seen this sort of assignment before and was wondering if this is just an alternative way of handling it. Is the above the same as this?
IFS=':' read -a param <<< $#
user="${1}"
pass="${2}"
user_folders="${3}"
(change in values to 1-3 due to ${0} being the name of the file itself). This script is 5 years old; This original sort of assignment just seems a longer way to to it, unless I've missed something
I'm still learning shell scripting but as I understand, setting IFS=':' will split the fields on : rather than whitespace and so in the examples, the value of "${param[0]}" and ${1} passed to the function would be user_one
Can someone please explain if there is a reason why "${param[0]}" should be used instead of ${1}?
The command:
IFS=':' read -a param <<< $#
reads the :-separated fields from the command arguments ($#) into the array variable named param. Bash arrays work just like lists in other languages, and you index them with brackets. ${param[0]} is the first field, ${param[1]} then next, and so on. Arrays like this can contain anything, and it's just because of the $# in the read command that this param array happens to contain the arguments. It could just as easily contain foo, bar, and baz if it were created like:
param=(foo bar baz)
The ${1}, ${2} etc. syntax always refers to the script arguments though.
Related
For debugging my scripts, I would like to add the internal variables $FUNCNAME and $LINENO at the beginning of each of my outputs, so I know what function and line number the output occurs on.
foo(){
local bar="something"
echo "$FUNCNAME $LINENO: I just set bar to $bar"
}
But since there will be many debugging outputs, it would be cleaner if I could do something like the following:
foo(){
local trace='$FUNCNAME $LINENO'
local bar="something"
echo "$trace: I just set bar to $bar"
}
But the above literally outputs:
"$FUNCNAME $LINENO: I just set bar to something"
I think it does this because double quotes only expands variables inside once.
Is there a syntactically clean way to expand variables twice in the same line?
You cannot safely evaluate expansions twice when handling runtime data.
There are means to do re-evaluation, but they require trusting your data -- in the NSA system design sense of the word: "A trusted component is one that can break your system when it fails".
See BashFAQ #48 for a detailed discussion. Keep in mind that if you could be logging filenames, that any character except NUL can be present in a UNIX filename. $(rm -rf ~)'$(rm -rf ~)'.txt is a legal name. * is a legal name.
Consider a different approach:
#!/usr/bin/env bash
trace() { echo "${FUNCNAME[1]}:${BASH_LINENO[0]}: $*" >&2; }
foo() {
bar=baz
trace "I just set bar to $bar"
}
foo
...which, when run with bash 4.4.19(1)-release, emits:
foo:7: I just set bar to baz
Note the use of ${BASH_LINENO[0]} and ${FUNCNAME[1]}; this is because BASH_LINENO is defined as follows:
An array variable whose members are the line numbers in source files where each corresponding member of FUNCNAME was invoked.
Thus, FUNCNAME[0] is trace, whereas FUNCNAME[1] is foo; whereas BASH_LINENO[0] is the line from which trace was called -- a line which is inside the function foo.
Although eval has its dangers, getting a second expansion is what it does:
foo(){
local trace='$FUNCNAME $LINENO'
local bar="something"
eval echo "$trace: I just set bar to $bar"
}
foo
Gives:
foo 6: I just set bar to something
Just be careful not to eval anything that has come from external sources, since you could get a command injected into the string.
Yes to double expansion; but no, it won't do what you are hoping for.
Yes, bash offers a way to do "double expansion" of a variable, aka, a way to first interpret a variable, then take that as the name of some other variable, where the other variable is what's to actually be expanded. This is called "indirection". With "indirection", bash allows a shell variable to reference another shell variable, with the final value coming from the referenced variable. So, a bash variable can be passed by reference.
The syntax is just the normal braces style expansion, but with an exclamation mark prepended to the name.
${!VARNAME}
It is used like this:
BAR="my final value";
FOO=BAR
echo ${!FOO};
...which produces this output...
my final value
No, you can't use this mechanism to do the same as $( eval "echo $VAR1 $VAR2" ). The result of the first interpretation must be exactly the name of a shell variable. It does not accept a string, and does not understand the dollar sign. So this won't work:
BAR="my final value";
FOO='$BAR'; # The dollar sign confuses things
echo ${!FOO}; # Fails because there is no variable named '$BAR'
So, it does not solve your ultimate quest. None-the-less, indirection can be a powerful tool.
I am fairly new to unix bash scripting and need to know if this is possible. I want to ask user for their input multiple times and then store that input in to one variable.
userinputs= #nothing at the start
read string
<code to add $string to $userinputs>
read string
<code to add $string to $userinputs> #this should add this input along with the other input
so if the user enters "abc" when asked first time, it add's "abc" in $userinputs
then when asked again for the input and the user enters "123" the script should store it in the same $userinputs
this would make the $userinput=abc123
The usual way to concat two strings in Bash is:
new_string="$string1$string2"
{} are needed around the variable name only if we have a literal string that can obstruct the variable expansion:
new_string="${string1}literal$string2"
rather than
new_string="$string1literal$string2"
You can also use the += operator:
userinputs=
read string
userinputs+="$string"
read string
userinputs+="$string"
Double quoting $string is optional in this case.
See also:
How to concatenate string variables in Bash?
You can concatentate variables and store multiple strings in the same one like so:
foo=abc
echo $foo # prints 'abc'
bar=123
foo="${foo}${bar}"
echo $foo # prints 'abc123'
You can use the other variables, or the same variable, when assigning to a variable, e.g. a="${a}123${b}". See this question for more info.
You don't have to quote the strings you're assigning to, or do the ${var} syntax, but learning when to quote and not to quote is a surprisingly nuanced art, so it's often better to be safe than sorry, and the "${var}" syntax in double quotes is usually the safest approach (see any of these links for more than you ever wanted to know: 1 2 3).
Anyway, you should read into a temporary variable (read, by default, reads into $REPLY) and concatentate that onto your main variable, like so:
allinput=
read # captures user input into $REPLY
allinput="${REPLY}"
read
allinput="${allinput}${REPLY}"
Beware that the read command behaves very differently depending on supplied switches and the value of the IFS global variable, especially in the face of unusual input with special characters. A common "just do what I mean" choice is to empty out IFS via IFS= and use read -r to capture input. See the read builtin documentation for more info.
This question already has answers here:
A confusion about ${array[*]} versus ${array[#]} in the context of a bash completion
(2 answers)
Closed 6 years ago.
When I get the content of an array in a string, I have the 2 solutions bellow :
$ a=('one' 'two')
$ str1="${a[*]}" && str2="${a[#]}"
After, of course, I can reuse my string on the code
but how can I know if my variable has only one or several words?
In both cases, the contents of the array are concatenated to a single string and assigned to the variable. The only difference is what is used to join the elements. With ${a[*]}, the first character of IFS is used. With ${a[#]}, a single space is always used.
$ a=(one two)
$ IFS="-"
$ str1="${a[*]}"
$ str2="${a[#]}"
$ echo "$str1"
one-two
$ echo "$str2"
one two
When expanding $str1 or $str2 without quoting, the number of resulting words is entirely dependent on the current value of IFS, regardless of how the variables were originally defined. "$str1" and "$str2" each expand, of course, to a single word.
To add to #chepner's great answer: the difference between ${arr[*]} and ${arr[#]} is very similar to the difference between $* and $#. You may want to refer to this post which talks about $* and $#:
What's the difference between $# and $* in UNIX?
As a rule of thumb, it is always better to use "$#" and "${arr[#]}" than their unquoted or * counterparts.
"${a[*]}" expands to one string for all entries together and "${a[#]}" expands to one string per entry.
Assume we had a program printParameters, which prints for each parameter ($1, $2, and so on) the string my ... parameter is ....
>_ a=('one' 'two')
>_ printParameters "${a[*]}"
my 1. parameter is one two
>_ printParameters "${a[#]}"
my 1. parameter is one
my 2. parameter is two
If you would expand the array manually, you would write
${a[*]} as "one two" and
${a[#]} as "one" "two".
There also differences regarding IFS and so on (see other answers). Usually # is the better option, but * is way faster – use the latter one in cases where you have to deal with large arrays and don't need separate arguments.
By the way: The script printParameters can be written as
#! /bin/bash
argIndex=0
for argValue in "$#"; do
echo "my $((++i)). argument is $argValue"
done
It's very useful for learning more about expansion by try and error.
So I'm trying to do something, not sure if it's possible. I have the following code:
for i in {0..5}; do
if [[ -f ./user$i ]]; then
group$i=$(grep -w "group" ./user0|awk '{print $2}'|perl -lape 's/\s+//sg')
What I want to do is assign a unique variable for each instance of the {0..5} so group1 group2 group3 group4 for each variable name. Then I would change ./user0 to ./user$i and create a dynamic list of variables based on my sequence.
Is this possible? I get the following error when trying to execute this and I'm unsure of what I have actually done that bash doesn't like.
test.sh: line 16: group0=j: command not found
Kurt Stutsman provides the right pointer in a comment on the question: use Bash arrays to solve your problem.
Here's a simplified example:
groups=() # declare an empty array; same as: declare -a groups
for i in {0..5}; do
groups[i]="group $i" # dynamically create element with index $i
done
# Print the resulting array's elements.
printf '%s\n' "${groups[#]}"
See the bottom of this answer for other ways to enumerate the elements of array ${groups[#]}.
bash arrays can be dynamically expanded (and can even be sparse - element indices need not be contiguous)
Hence, simply assigning to element $i works, without prior sizing of the array.
Note how $i need not be prefixed with $ in the array subscript, because array subscripts are evaluated in an arithmetic context (the same context in which $(( ... )) expressions are evaluated).
As for what you did wrong:
group$i=...
is not recognized as a variable assignment by Bash, because - taken literally - group$i is not a valid identifier (variable name).
Because it isn't, Bash continues to parse until the next shell metacharacter is found, and then interprets the resulting word as a command to execute, which in your case resulted in error message group0=j: command not found.
If, for some reason, you don't want to use arrays to avoid this problem entirely, you can work around the problem:
By involving a variable-declaring builtin [command] such as declare, local, or export, you force Bash to perform expansions first, which expands group$i to a valid variable name before passing it to the builtin.
user2683246's answer demonstrates the next best approach by using declare (or, if local variables inside a function are desired, local) to create the variables.
Soren's answer uses export, but that is only advisable if you want to create environment variables visible to child processes rather than mere shell variables.
Caveat: With this technique, be sure to double-quote the RHS in order to capture the full value; to illustrate:
i=0; declare v$i=$(echo 'hi, there'); echo "$v0" # !! WRONG -> 'hi,': only UP TO 1ST SPACE
i=0; declare v$i="$(echo 'hi, there')"; echo "$v0" # OK -> 'hi, there'
Other ways to enumerate the groups array created above:
# Enumerate array elements directly.
for element in "${groups[#]}"; do
echo "$element"
done
# Enumerate array elements by index.
for (( i = 0; i < ${#groups[#]}; i++ )); do
echo "#$i: ${groups[i]}"
done
Use declare group$i=... instead of just group$i=...
Try to use the export or declare function like this
for i in {0..5}; do
if [[ -f ./user$i ]]; then
export group$i=$(grep -w "group" ......
with declare
for i in {0..5}; do
if [[ -f ./user$i ]]; then
declare group$i=$(grep -w "group" ......
where export makes the value available to sub-processes, and declare just available within the same script.
Normally when a parameter is passed to a shell script, the value goes into ${1} for the first parameter, ${2} for the second, etc.
How can I set the default values for these parameters, so that if no parameter is passed to the script, we can use a default value for ${1}?
You can't, but you can assign to a local variable like this: ${parameter:-word} or use the same construct in the place you need $1. this menas use word if _paramater is null or unset
Note, this works in bash, check your shell for the syntax of default values
You could consider:
set -- "${1:-'default for 1'}" "${2:-'default 2'}" "${3:-'default 3'}"
The rest of the script can use $1, $2, $3 without further checking.
Note: this does not work well if you can have an indeterminate list of files at the end of your arguments; it works well when you can have only zero to three arguments.
#!/bin/sh
MY_PARAM=${1:-default}
echo $MY_PARAM
Perhaps I don't understand your question well, yet I would feel inclined to solve the problem in a less sophisticated manner:
! [[ ${1} ]] && declare $1="DEFAULT"
Hope that helps.