How does the "read" command work in Bash functions - bash

Just starting to use bash for the first time (so apologies if this is a dumb question).
As far as I've understood, read is used to receive user input. However, in the example below, it seems that it's also being used to assign function arguments.
Am I missing something here? Is there something else going on? I'm finding it hard to find documentation about how this works.
Any help would be appreciated
function server () {
while true
do
read method path version
if $method = 'GET'
then
echo 'HTTP/1.1 200 OK'
else
echo 'HTTP/1.1 400 Bad Request'
fi
done
}

In your example, the read command is used to read input into 3 different variables method, path and version.
If the user enters as input the line "my name is joe", then the values of method, path and version are:
method -> "my"
path -> "name"
version -> "is joe"
If, however, the user enters "hello world", then only method and path will contain a string. Specifically:
method -> "hello"
path -> "world"
version ->
If you want to use the read command to read input into N variables, i.e. read var1 var2 ... varN, the user's input on the command line will be split/ delimited by the characters in the IFS variable. In fact, IFS stands for "Input Field Separators".
By default, the IFS variable is equivalent to $' \t\n'. This means that unless otherwise specified, user inputs are delimited by space, tab or the newline character.
EDIT: IFS is also commonly referred to as the "Internal Field Separator" as David pointed out in the comments (if you see this from other developers).

Related

How to grab value back from external script in bash?

I'm sure I'm missing something stupid. I want to pass a full path variable to a perl script, where I do some work on it and then pass it back. So I have:
echo "Backing up: $f ";
$write_file="$(perl /home/spider/web/foo.com/public_html/gen-path.cgi $f)";
echo "WRITE TO: $write_file \n";
However, this gives me:
Backing up: /home/spider/web/foo.com/public_html/websites-uk/uk/q/u
backup-files-all.sh: line 7: =backup-uk-q-u.tar.gz: command not found
WRITE TO: \n
I can't work out why its not saving the output into $write_file. I must be missing something (bash isn't my prefered language, which is why I'm passing to Perl as I'm a lot more fluent in that :))
Unless your variable write_file already exists, the command $write_file="something" will translate to ="something"(1).
When setting a variable, leave off the $ - you only need it if you want the value of the variable.
In other words, what you need is (note no semicolons needed):
write_file="$(perl /home/spider/web/foo.com/public_html/gen-path.cgi $f)"
(1) It can be even hairier if it is set to something. For example, the code:
write_file=xyzzy
$write_file="something"
will result in something being placed into a variable called xyzzy, not write_file :-)

Delayed expansion of composite variable in Bash

I'm defining a variable as a composition of other variables and some text, and I'm trying to get this variable to not expand its containing variables on the assigning. But I want it to expand when called later. That way I could reuse the same template to print different results as the inner variables keep changing. I'm truing to avoid eval as much as possible as I will be receiving some of the inner variables from third parties, and I do not know what to expect.
My use case, as below, is to have some "calling stack" so I can log all messages with the same format and keep a record of the script, function, and line of the logged message in some format like this: script.sh:this_function:42.
My attempted solution
called.sh:
#!/bin/bash
SCRIPT_NAME="`basename "${BASH_SOURCE[0]}"`"
CURR_STACK="${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${CURR_STACK}"
echo
function _func_1 {
echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${CURR_STACK}"
}
_func_1
So, I intend to get the same results while printing the "${CURR_STACK}" as when printing the previous line.
If there is some built-in or other clever way to log this 'call stack', by all means, let me know! I'll gladly wave my code good-bye, but I'd still like to know how to prevent the variables from expanding right away on the assigning of CURR_STACK, but still keep them able to expand further ahead.
Am I missing some shopt?
What I've tried:
Case 1 (expanding on line 4):
CURR_STACK="${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]}"
CURR_STACK="`echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"`"
CURR_STACK="`echo "\${SCRIPT_NAME}:\${FUNCNAME[0]}:\${LINENO[0]}"`"
called.sh::7 <------------------| These are control lines
called.sh::4 <---------------. .------------| With the results I expect to get.
X
called.sh:_func_1:12 <---´ `-------| Both indicate that the values expanded
called.sh::4 <-------------------------| on line 4 - when CURR_STACK was set.
Case 2 (not expanding at all):
CURR_STACK="\${SCRIPT_NAME}:\${FUNNAME[0]}:\${LINENO[0]}"
CURR_STACK=\${SCRIPT_NAME}:\${FUNCNAME[0]}:\${LINENO[0]}
CURR_STACK="`echo '${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}'`"
called.sh::7
${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]} <-------.----| No expansion at all!...
/
called.sh::12 /
${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]} <----´
Shell variables are store plain inert text(*), not executable code; there isn't really any concept of delayed evaluation here. To make something that does something when used, create a function instead of a variable:
print_curr_stack() {
echo "$(basename "${BASH_SOURCE[1]}"):${FUNCNAME[1]}:${BASH_LINENO[0]}"
}
# ...
echo "We are now at $(print_curr_stack)"
# Or just run it directly:
print_curr_stack
Note: using BASH_SOURCE[1] and FUNCNAME[1] gets info about context the function was run from, rather than where it is in the function itself. But for some reason I'm not clear on, BASH_LINENO[1] gets the wrong info, and BASH_LINENO[0] is what you want.
You could also write it to allow the caller to specify additional text to print:
print_curr_stack() {
echo "$#" "$(basename "${BASH_SOURCE[1]}"):${FUNCNAME[1]}:${BASH_LINENO[0]}"
}
# ...
print_curr_stack "We are now at"
(* There's an exception to what I said about variables just contain inert text: some variables -- like $LINENO, $RANDOM, etc -- are handled specially by the shell itself. But you can't create new ones like this except by modifying the shell itself.)
Are you familiar with eval?
$ a=this; b=is; c=a; d=test;
$ e='echo "$a $b $c $d"';
$ eval $e;
this is a test
$ b='is NOT'; # modify one of the variables
$ eval $e;
this is NOT a test
$ f=$(eval $e); # capture the value of the "eval" statement
$ echo $f;
this is NOT a test

What is the difference between ${var:-word} and ${var-word}?

I found the following command in a bash script:
git blame $NOT_WHITESPACE --line-porcelain "${2-#}" -- "$file"
What does this ${2-#} mean? Trying out, it returns the 2nd argument, and "#" if it doesn't exist. According to the documentation, ${2:-#} should do the same. I tried it, and it indeed does the same. What's the difference? Where is it documented? The man page does not seem to say anything about this notation.
From Bash hackers wiki - parameter expansion:
${PARAMETER:-WORD}
${PARAMETER-WORD}
If the parameter PARAMETER is unset (never was defined) or null
(empty), this one expands to WORD, otherwise it expands to the value
of PARAMETER, as if it just was ${PARAMETER}. If you omit the :
(colon), like shown in the second form, the default value is only used
when the parameter was unset, not when it was empty.
echo "Your home directory is: ${HOME:-/home/$USER}."
echo "${HOME:-/home/$USER} will be used to store your personal data."
If HOME is unset or empty, everytime you want to print something
useful, you need to put that parameter syntax in.
#!/bin/bash
read -p "Enter your gender (just press ENTER to not tell us): " GENDER
echo "Your gender is ${GENDER:-a secret}."
It will print "Your gender is a secret." when you don't enter the
gender. Note that the default value is used on expansion time, it is
not assigned to the parameter.

Read input, save it to a dynamically-named variable and check if given input was empty

Consider a generic ask() function that asks the user a question, reads the input and saves it in a variable named according to one of the function's arguments.
ask() {
local question="$1"
local varname="$2"
echo "$question"
read $varname
}
Suppose I want to ask the user what is his favourite pet and store the answer in a variable named $pet. Usage would be as follows:
ask "What is your favourite pet?" pet
What I want to do and need help with is check if the user's input was empty, and in that case set the user's input to some string. I would be able to do this easily if the name of the variable the user's input is stored in was constant, like so:
if [ -z "$pet" ]; then
pet="foo"
fi
However the name of the variable I want to check whether or not is empty is whatever I pass in as the second argument. How can I check if the variable (named as per the value of $varname) containing the user's input is empty? The solution should be as portable and standard as possible, and must work under bash and zsh specifically.
In bash, ${!varname} gives you the value of the variable whose name is the value of $varname, but as far as I know, this syntax is not supported by zsh. If you want something that works in both bash and zsh, you may have to use the oldfashioned eval value=\${$varname} and then check $value. You should only use this if you know in advance that the value of $varname is a legal variable name; otherwise this is unsafe.
maybe:
ask() {
name=$1;shift
read -r -p "$# >" var
eval "$name='$var'"
}
ask pet "What is your favourite pet?"
pet=${pet:-foo}
echo "PET: $pet"
Based on the input thus far I managed to get a satisfying solution.
eval varname_tmp=\$$varname
if [ -z "$varname_tmp" ]; then
eval "$varname=foo"
fi

Variables from stdin

I want to get an input from the user using read
read line
and the proper input would be a string and then a number:
a 5
b 6
+ 87
How do you separate the "a" and 5 into two variables, with the 5 into a integer variable?
read supports the command line option -a, to store the input in an array like that:
$ read -a line
a 4
$ echo ${line[0]}
a
$ echo ${line[1]}
4
That would be nicer than using two variables, in my opinion.
I suggest reading the documenentation of read to get you started:
help read
Here are the first two paragraphs:
Read a line from the standard input and split it into fields.
Reads a single line from the standard input, or from file descriptor FD
if the -u option is supplied. The line is split into fields as with word
splitting, and the first word is assigned to the first NAME, the second
word to the second NAME, and so on, with any leftover words assigned to
the last NAME. Only the characters found in $IFS are recognized as word
delimiters.
Note that bash doesn't have a notion of "integer variables" comparable to other programming languages. Bash variables are untyped. Declaring a variable as integer using declare -i only influences assignments to this variable -- everything that is not a valid integer is silently set to 0.
I presume you're operating in the shell since this post is tagged "bash" but you might want to make that explicit.
Anyway, the "read" command to the shell takes multiple variable names, not just one. You can give it two and it will hand each word on the line to you in the respective variables. (They're split on the field separator given by the IFS variable.)
The shell doesn't really have any distinction between "integer" variables and any other kind in the general case.
I suggest reading the man page for the shell fully if you really want to understand how to write shell scripts properly.
The read command will split your input along whatever is in $IFS. This, by default is whitespace, so simply doing this:
read my_string my_number
will split your input into two sections separated by the space. Sometimes, you'll see this:
read my_string my_number garbage
Because read will read in the entire rest of the line into the last variable no matter how many parameters you had. For example, if I had:
read my_string my_number
And the user put in:
this 1 foo foo foo!
$my_string will be this, but $my_number will be 1 foo foo foo!
By putting another variable (called garbage in this case), I eliminate this issue. If I put:
read my_string my_number garbage
And the user put in:
this 1 foo foo foo!
$my_string will be this, $my_number will be 1, and $garbage would be foo foo foo!.
A simple test program:
while read my_string my_number garbage
do
echo "The string is '$my_string'. The number is '$my_number'."
echo "Note there's no error checking of input."
echo "That's your job, Bunky."
done

Resources