Can anyone help me with this code? bash doesn't recognize the $2 only the first $1 show an error: read: '2': it is not a valid identificator.
#!/bin/bash
read $#
a=$#
You cannot read into $#, or into the variable called 2 (which $# expands to).
Instead, to reassign $2, you need to use set to completely rewrite the full set of positional parameters:
set -- one two
...will set $2 to two, and $# to 2 (since two items were provided).
By contrast, if you simply wish to use the value for $2 passed on your script's command line, you don't need to (and shouldn't) use read at all.
By contrast, if you want to access the last command-line argument, you can use indirect expansion for that:
set -- one two last
last_arg=$# # sets last_arg=3
result=${!last_arg} # sets result=last
...or, if you want to overwrite the last command-line argument with a value read from stdin:
read new_last
set -- "${#:1:$(( $# - 1 ))}" "$new_last"
Related
I'm writing a bash script that uses rsync to synchronize directories. According to the Google shell style guide:
Always quote strings containing variables, command substitutions, spaces or shell meta characters, unless careful unquoted expansion is required.
Use "$#" unless you have a specific reason to use $*.
I wrote the following test case scenario:
#!/bin/bash
__test1(){
echo stdbuf -i0 -o0 -e0 $#
stdbuf -i0 -o0 -e0 $#
}
__test2(){
echo stdbuf -i0 -o0 -e0 "$#"
stdbuf -i0 -o0 -e0 "$#"
}
PARAM+=" --dry-run "
PARAM+=" mirror.leaseweb.net::archlinux/"
PARAM+=" /tmp/test"
echo "test A: ok"
__test1 nice -n 19 rsync $PARAM
echo "test B: ok"
__test2 nice -n 19 rsync $PARAM
echo "test C: ok"
__test1 nice -n 19 rsync "$PARAM"
echo "test D: fails"
__test2 nice -n 19 rsync "$PARAM"
(I need stdbuf to immediately observe output in my longer script that i'm running)
So, my question is: why does test D fail with the below message?
rsync: getaddrinfo: --dry-run mirror.leaseweb.net 873: Name or service not known
The echo in every test looks the same. If I'm suppose to quote all variables, why does it fail in this specific scenario?
It fails because "$PARAM" expands as a single string, and no word splitting is performed, although it contains what should be interpreted by the command as several arguments.
One very useful technique is to use an array instead of a string. Build the array like this :
declare -a PARAM
PARAM+=(--dry-run)
PARAM+=(mirror.leaseweb.net::archlinux/)
PARAM+=(/tmp/test)
Then, use an array expansion to perform your call :
__test2 nice -n 19 rsync "${PARAM[#]}"
The "${PARAM[#]}" expansion has the same property as the "$#" expansion : it expands to a list of items (one word per item in the array/argument list), no word splitting occurs, just as if each item was quoted.
I agree with #Fred — using arrays is best. Here's a bit of explanation, and some debugging tips.
Before running the tests, I added
echo "$PARAM"
set|grep '^PARAM='
to actually show what PARAM is.** In your original test, it is:
PARAM=' --dry-run mirror.leaseweb.net::archlinux/ /tmp/test'
That is, it is a single string that contains multiple space-separated pieces.
As a rule of thumb (with exceptions!*), bash will split words unless you tell it not to. In tests A and C, the unquoted $# in __test1 gives bash an opportunity to split $PARAM. In test B, the unquoted $PARAM in the call to __test2has the same effect. Therefore,rsync` sees each space-separated item as a separate parameter in tests A-C.
In test D, the "$PARAM" passed to __test2 is not split when __test2 is called, because of the quotes. Therefore, __test2 sees only one parameter in $#. Then, inside __test2, the quoted "$#" keeps that parameter together, so it is not split at the spaces. As a result, rsync thinks the entirety of PARAM is the hostname, so fails.
If you use Fred's solution, the output from sed|grep '^PARAM=' is
PARAM=([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")
That is bash's internal notation for an array: PARAM[0] is "--dry-run", etc. You can see each word individually. echo $PARAM is not very helpful for an array, since it only outputs the first word (here, --dry-run).
Edits
* As Fred points out, one exception is that, in the assignment A=$B, B will not be expanded. That is, A=$B and A="$B" are the same.
** As ghoti points out, instead of set|grep '^PARAM=', you can use declare -p PARAM. The declare builtin with the -p switch will print out a line that you could paste back into the shell to recreate the variable. In this case, that output is:
declare -a PARAM='([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")'
This is a good option. I personally prefer the set|grep approach because declare -p gives you an extra level of quoting, but both work fine. Edit As #rici points out, use declare -p if an element of your array might include a newline.
As an example of the extra quoting, consider unset PARAM ; declare -a PARAM ; PARAM+=("Jim's") (a new array with one element). Then you get:
set|grep: PARAM=([0]="Jim's")
# just an apostrophe ^
declare -p: declare -a PARAM='([0]="Jim'\''s")'
# a bit uglier, in my opinion ^^^^
I need to print the argument number of which user types in. No matter what I do I always get just an empty line
echo "Give argument number"
read number
allV=$#
echo ${allV[$number]}
what is wrong with this few lines? Even if I start the script with a few arguments and I just manually write sth like"
echo ${allV[1]}
again all I get is an empty line.
Bash lets you use an indirect reference, which works also on numbered parameters:
echo "${!number}"
It also lets you slice the argument list:
echo "${#:$number:1}"
Or you could copy the arguments into an array:
argv=("$#")
echo "${argv[number]}"
In all cases, the quotes are almost certainly required, in case the argument includes whitespace and/or glob characters.
To handle $# as an array, just change it to ("$#") :
echo "Give argument number"
read number
allV=("$#")
echo ${allV[$number-1]}
My script is called by a program that generates argument randomly such as
input=12 output=14 destinationroute=10.0.0.0
and then calls my script with the generated arguments:
./getroute.sh input=12 output=14 destinationroute=10.0.0.0
Inside the script is like:
#!/bin/bash
input=$1
output=$2
destinationroute=$3
...
The program always calls arguments in random order (ex. input=12 output=14 or output=14 input=12), and I can't change the program.
Is there any way to recognize the correct parameters and put them in their proper place.
Don't rely on order if they aren't in order. Just iterate over the arguments, look at which patterns they match, and assign to a variable appropriately:
for arg; do # default for a for loop is to iterate over "$#"
case $arg in
'input='*) input=${arg#*=} ;;
'output='*) output=${arg#*=} ;;
'destinationroute='*) destinationroute=${arg#*=} ;;
esac
done
If, for some reason, you really wanted to update $1, $2, and $3, though, you can do that by putting the following code after the above loop:
set -- "$input" "$output" "$destinationroute"
you need to call your function differently; For exmple:
./getroute.sh -i 12 -o 14 -d 10.0.0.0
Then inside your script use getopt to read the variables.
Edit:
My scripting knowledge is not strong; therefore, there should be a better way to do it.
As you don't have access to the Program, you can add some lines inside your script to get the inputs; for example:
input=`echo $* | grep -E -o "input=[0-9]{2}" | awk -F"=" {'print$2'}`
You can do the same thing for other variables.
Is it possible to create a script that will prompt a user to enter a number that will then programmatically be used for the number of arguments in a function/method? For example:
echo "Enter Number of arguments: "
read numOfArguments
Once a number is entered, this will stand for the number of parameters in a function. So if I enter 5, there should be 5 expected parameters within that method once called:
sampleMethod(){
...Takes 5 Parameters and actual code is here
}
sampleMethod ActualArgumentsfromPrompt
If this is possible can someone provide an example of how this is done? I am doing this because I would like to create a script that will parse through a log file but I need to pull specific items from the log that are set up similar to this: (0010,0020). The information I need will not always be a fixed number of items so that's why I would like to generate a prompt for a number of arguments.
Bash functions don't "expect" any number of arguments -- as you can see from the syntax, there's no list of parameter variables after the function name. They process arguments the same way the main shell does, by accessing them as $1, $2, etc. If you need to know how many arguments were passed, you can use $#.
To loop through all arguments, the common idiom is to process $1, then use shift to shift the argument list down.
sampleMethod() {
if [ $# -ne $numOfArguments ]
then echo Wrong number of arguments -$numOfArguments expected, $# provided >&2
return
fi
while [ $# -gt 0 ]; do
arg=$1
echo "$arg"
shift
done
}
x11-common package installs a /etc/X11/Xsession.d/20x11-common_process-args script which is sourced in by /etc/X11/Xsession. This 20x11-common_process-args script contains following if-statement:
has_option() {
if [ "${OPTIONS#*
$1}" != "$OPTIONS" ]; then
return 0
else
return 1
fi
}
OPTIONS variable is a list of configuration options from a file separated by line-feeds(0a in ASCII). How to understand this if-statement? Literally, this parameter expansion part should modify the OPTIONS variable in a way that everything before the argument($1) is removed? This argument needs to match one of the configuration options. However, what is the general meaning of this if-statement?
This fragment of code:
xyz=gobbledegook
echo ${xyz#*de}
echoes "gook", so the ${OPTIONS#*$1} notation deletes everything from the start of $OPTIONS up to and including the $1.
In the script, it is checking whether $1 (the first argument to the function) is present in the list of options in $OPTIONS. If the value is different, then the option is matched; otherwise, it isn't. It's a fairly compact way of dealing with a lot of options all at once.
The newline in the test (well, strictly, it is in the string that is present in the test) is unorthodox but legitimate.
You can easily experiment for yourself, of course:
$ OPTIONS="-abc
> -def
> -ghi"
$ echo "${OPTIONS#*-abc}"
-def
-ghi
$ echo "${OPTIONS#*-def}"
-ghi
$ echo "${OPTIONS#*-ghi}"
$ echo "${OPTIONS#*-xyz}"
-abc
-def
-ghi
$
You are correct that the parameter expansion removes everything up to and including $1 from the expansion. If $1 is not present, then the expansion removes nothing, and the expansion is identical to simply expanding $OPTIONS without modification. So, the if statement simply returns 0 if OPTIONS contains $1 (that is, the two expansions are different), and returns 1 if it does not contain $1 (that is, the two expansions are the same).
In broader terms, has_option foo succeeds if foo is present in $OPTIONS, and fails if foo is not.