I expect the following snippet
until [ MY_VAR=$(echo my_var_val) ]
do
echo 'inside the loop'
done
echo $MY_VAR
to produce the my_var_val output. However, this does not happen. How can I make this happen?
Long story
I want to perform a retry on a script producing some output. I need the output later in the script. Unfortunately, assigning the variable value in the until test fails - the variable has empty value, when I try to use it later in the script. How can I execute the external script with retry logic and have its output stored in a variable that I can use later in the script?
max_retry=10
counter=0
until [ IMPORTANT_SCRIPT_OUTPUT=$(python very_important_script_with_output.py) ]
do
#retry logic
if [[ counter -eq $max_retry ]]; then
echo "Failed"
exit 1
fi
((counter++))
echo "very_important_script_with_output failed, retrying"
done
python another_very_important_script_with_the_previous_script_output_as_parameter.py --important-parameter $IMPORTANT_SCRIPT_OUTPUT
You can try this:
until MY_VAR=$(echo my_var_val); test -n "$MY_VAR"
do
echo 'inside the loop'
done
echo $MY_VAR
[ is an ordinary command, not shell syntax, and its arguments are processed normally. So MY_VAR=$(echo my_var_val) is just a string beginning with MY_VAR=, not a variable assignment.
Do the assignment separately from testing the variable.
while :
do
MY_VAR=$(command)
if [[ -n "$MY_VAR" ]]
then break
fi
echo 'inside the loop
done
Related
Let assume I have something like below:
eval link='/var/lib/${XYZ}/test' # string from another text file
XYZ is just for the the example and it could be anything like below:
eval link='/var/lib/${MY_OWN_VAR}/test' # single quote here not double quotes
eval link='/var/lib/${WHAT_EVER}/test'
Is it possible to error out if XYZ is not set? or is there any other way to figure out if XYZ is set or not?
I have looked at this, but the assumption there is that you know the variable name. In my case I have no control over what would be in the string to be evaluated.
UPDATE
To be clear, all the strings that needs to be evaluated are from a text file. basically a program reads a text file and
outputs the evaluated strings.
All I am trying here is to figure out a way to gracefully catch "unbound variable" error while evaluating any string. basically what set -u does but gracefully.
You can test the eval in a subshell before performing it for real:
assign_if_defined(){
echo 1>&2 "testing $1=$2"
outvar="$1"
input=${2#Q}
err=$(exec 2>&1; set -u; eval "${outvar}=${input#P}")
if [ -z "$err" ]; then
echo 1>&2 "eval test succeeded: doing for real"
eval "${outvar}=${input#P}"
else
echo 1>&2 "eval test failed: not doing for real"
echo 1>&2 "error: $err"
fi
}
A=set
assign_if_defined link1 "'"'"\/${A}/'
echo link1=$link1
unset B
assign_if_defined link2 '/$B/'
echo link2=$link2
It seems that the #Q/#P transformations first appeared in bash 4.4. Using them means that quoting is much simplified. If using an older version of bash, you could try normal quoting (eval "${outvar}=\"${input}\"") but the code will fail if input contains special characters (as the first example).
Well I don't know exactly how much control (or knowledge) you do have on the strings, but can't you just test if it's empty?
VAR=mydirectory
str=/var/lib/${VAR}/test # valid
str2=/var/lib/${NONEXISTANT}/test # invalid
if [[ "$str" = "/var/lib//test" ]] ;
then
echo 'is_empty';
else
echo 'is_set';
fi;
The only downfall is that the test will fail if you receive a var that is set but empty, e.g. VAR=""
I'm tasked with writing a shell script that takes in a string of 6 letters and numbers and checks if they meet a certain criteria.
This is that script
FILE=$1
var=${#FILE}
if [ $var -gt 6 ] || [ $var -lt 6 ]
then
#echo $FILE "is not a valid NSID"
exit 1
else if ( echo $1 | egrep -q '^[A-Za-z]{3}\d{3}$' )
then
#echo $1 "is a valid NSID"
exit 0
else
#echo $1 "is not a valid NSID"
exit 1
fi
fi
It works. so that isn't where the problem is.
What I am trying to do is use a "wrapper" script to gather potential valid NSID's from a text file and call that script on the list of inputs. so that if I call that script within my wrapper script it will step through the text file I have given my wrapper and check if each line is valid.
FILE=$1
YES= more $FILE
if ( exec /path/to/file/is_nsid.sh $YES -eq 0 )
then
echo $FILE "is a valid NSID"
else
echo $FILE "is not a valid NSID"
fi
so if I called it with a text file called file1.txt which contained
yes123
yess12
ye1243
it would output whether each was valid or not.
The line
YES= more $FILE
Sets YES in the environment passed to the command more $FILE. That's probably not what you intended.
The line
if ( exec /path/to/file/is_nsid.sh $YES -eq 0 )
starts a subshell to execute exec /path/to/file/is_nsid.sh $YES -eq 0. (That's what the parentheses do.) exec then replaces the subshell with a process which executes
/path/to/file/is_nsid.sh $YES -eq 0
which in turn runs the script at is_nsid.sh, passing it two or three command line arguments:
the value of $YES. This could be several arguments if the value of the shell variable includes whitespace or a glob symbol, but in this case it is more likely to be nothing since $YES has not been defined.
-eq
0
Since your script only examines its first argument, that's probably equivalent to
/path/to/file/is_nsid.sh -eq
That will, presumably, terminate with a failure status code, and since the subshell has been replaced with the script execution, that will also be the return status of the subshell. (Without exec, there would be essentially no difference; the subshell's return status is that of the last command executed in the subshell. Without either the parentheses or the exec, the result would also be the same. So you could have just written if /path/to/file/is_nsid.sh $YES -eq 0 and it would produce the same incorrect result.)
What you presumably wanted to do was to read each line in the file whose name is passed as the first command-line argument to the script. You could do that as follows:
while read -r line; do
if /path/to/file/is_nsid.sh "$line"; then
echo "$line is a valid NSID"
else
echo "$line is not a valid NSID"
fi
done < "$1"
You could simplify your is_nsid script considerably. The following is equivalent:
[ $#1 -eq 6 ] && echo "$1" | egrep -q '^[A-Za-z]{3}\d{3}$'
Note that \d is a Gnu extension to egrep and should not be relied on in portable code (which I assume this is trying to be). You should use [0-9] or [[:digit:]] instead.
The length check is actually unnecessary since the regex can only match six-character lines. Personally, I'd leave it out and just use
echo "$1" | egrep -q '^[[:alpha:]]{3}[[:digit:]]{3}$'
I removed all the unnecessary if statements. If I had left them in, I would have changed else if ... then ... fi to simply elif ... then ... to avoid unnecessary if nesting.
The code
In the example function a, I capture the input from a pipe as follows:
function a() {
if [ -t 1 ]; then
read test
echo "$test"
fi
if [[ -z "$1" ]]; then
echo "$1"
fi
}
called as follows:
echo "hey " | a "hello"
produces the output:
hey
hello
The issue
I was inspired by this answer, however a quote after the snippet has me concerned:
But there's no point - your variable assignments may not last! A pipeline may spawn a subshell, where the environment is inherited by value, not by reference. This is why read doesn't bother with input from a pipe - it's undefined.
I'm not sure I understand this - attempting to create subshells yielded the output I expected:
function a() {
(
if [ -t 1 ]; then
read test
echo "$test"
fi
if [[ -z "$1" ]]; then
echo "$1"
fi
)
}
And in the method call:
(echo "hey") | (a "hello")
still yields:
hey
hello
So what is meant by your variable assignments may not last! A pipeline may spawn a subshell, where the environment is inherited by value, not by reference.? Is there something that I've misunderstood?
The quoted note is incorrect. read doesn't care where its input comes from.
However, you must remember that the variable assigned to by the invocation of the read command is part of the (sub-)shell which executes the command.
By default, each command executed in a pipeline (a series of commands separated by |) is executed in a separate subshell. So after you execute echo foo | read foo, you will find that the value of $foo has not changed: not because read ignored its input but rather because the shell read executed in no longer exists.
Try this:
echo test | read myvar
echo $myvar
You might expect that it will print test, but it doesn't, it prints nothing. The reason is that bash will execute the read myvar in a subshell process. The variable will be read, but only in that subshell. So in the original shell the variable will never be set.
On the other hand, if you do this:
echo test | { read myvar; echo $myvar; }
or this
echo test | (read myvar; echo $myvar)
you will get the expected output. This is what happens with your code.
I am trying to write a little script, and I can not figure out how to choose the variable to be echo'ed (echo $'TEST'_"$response") dynamically depending on the user's input:
#!/usr/bin/env sh
response=response
TEST_1="Hi from 1!"
TEST_2="Hi from 2!"
while [ $response ]; do
read -p "Enter a choice between 1 - 2, or 'bye': " response
if [ $response = 'bye' ]; then
echo "See You !"; exit
elif [ $response -ge 1 ] && [ $response -le 2 ]; then
echo $'TEST'_"$response"
else
echo "Input is not a valid value."
fi
done
The desired output would be the value of one of the variables declared at the beginning of my script ("Hi from 1!" or "Hi from 2!"). Instead my script simple outputs the name of the variable as a string "TEST_1" or "TEST_2". I do not simply want to hardcode the variable that will be printed like:
if [ $response -ge 1 ]; then
echo $TEST_1
fi
since it is not scalable. Using backticks like
echo `$'TEST'_"$response"`
doesn't help either since bash will expect to run the result "TEST_1" or "TEST_2" as a command.
Any hint will be greatly appreciated.
You need indirect expansion, to be used with ${!var}:
$ TEST1="hello"
$ TEST2="bye"
$ v=1
$ var="TEST$v" #prepare the variable name of variable
$ echo ${!var} #interpret it
hello
$ v=2
$ var="TEST$v" #the same with v=2
$ echo ${!var}
bye
That is, you need to use a variable name of a variable and this is done with the indirect expansion: you use a variable with the name of the variable and then you evaluate it with the ${!var} syntax.
In your case, use:
myvar="TEST$response"
echo "${!myvar}"
Always use quotes, such as "$string", for anything other than numbers. For numbers, just keep it normal (i.e. $number).
I am making a bash script that you have to give 2 files or more as arguments.
I want to test if the given files exist. I'm using a while loop because I don't know how many files are given. The problem is that the if statement sees the $t as a number and not as the positional parameter $number. Does somebody have a solution?
t=1
max=$#
while [ $t -le $max ]; do
if [ ! -f $t ]; then
echo "findmagic.sh: $t is not a regular file"
echo "Usage: findmagic.sh file file ...."
exit
fi
t=`expr $t + 1`
done
You can do it with the bash Special parameter # in this way:
script_name=${0##*/}
for t in "$#"; do
if [ ! -f "$t" ]; then
echo "$script_name: $t is not a regular file"
echo "Usage: $script_name file file ...."
exit 1
fi
done
With "$#" you are expanding the positional parameters, starting from one as separate words (your arguments).
Besides, remember to provide a meaningful exit status (e.g. exit 1 instead of exit alone). If not provided, the exit status is that of the last command executed (echo in your case, which succes, so you're exiting with 0).
And for last, instead of write the script name (findmagic.sh in your case), you can set a variable at the beginning in your script:
script_name=${0##*/}
and then use $script_name when necessary. In this way you don't need to update your script if it changes its name.