mutiple returns from a function in a shell script - shell

function get_arguments()
{
read -p 'data : ' data
read -p 'lambda: ' lambda
echo $data $lambda
}
data,lambda=$(get_arguments)
But i am getting an error
data : /home/wolfman/Downloads/data
lambda value: 2
./shell_script.sh: line 25: data,lambda,= /home/wolfman/Downloads/data: No such file or directory
But
1) Why is it even evaluating that whether that file exists or not.. its just a string??
2) what am i doing wrong :(
THanks

sh syntax does not allow that. But, the variables in the function are global, so you can just invoke the function and data and lambda will be set in the caller.
functions return an integer value, but they can print arbitrary data which can be read by the caller. For example, you could do:
get_arguments | { read data lambda; echo $data $lambda; }
The drawback is that the values are only available in that block. (The pipe creates a subshell, and the values read by read are only valid in that subshell.)

Just for fun here are a couple of other possible methods.
read -r data lambda <<< $(get_arguments)
or
set -- $(get_arguments)
data=$1
lambda=$2

shells don't allow direct assignment to lists of variables, you have manage that with shell string parsing (or possibly other methods). Try
data_lambda=$(get_arguments)
data=${data_lambda% *}
#-----------------^^space char
lambda=${data_lambda#* }
#------------------^^space char
$d=123 l=345
$data_lambda=$(echo $d $l)
$echo $data_lambda
123 345
$data=${data_lambda% *}
$lambda=${data_lambda#* }
$echo $data
123
$echo $lambda
345
Substituting $(echo $d $l) for data_lambda=$(get_arguments)`.
See my write-up on shell parameter modifiers
IHTH

Related

passing bash array separated by comma

i have bash script which do some function with website domains like following
#!/usr/bin/env bash
domain="$1";
function A(){
ping $domain -c 4
}
function B(){
host $domain
}
A;
B;
and when i run this script by doing ./script.sh whateverdomain.com it works well.
BUT i heard that with array i can run the script against some of domains seperated by comma for example.
like ./script.sh domain1.com,domain2.com and it will excute the whole script function against the first one then the second one and i tried the following code.
my_arr=($(echo $DOMAIN | tr "," "\n"))
d=$(for domain in "${my_arr[#]}" ;
do echo $domain
done)
pingme(){
ping -c 4 $d
}
but it hanging and not passing each domain to variable $d
so i need to define array as function and when i run the script it pass domain and execute the script functions then repeat the whole script against the second domain and so on like.
#!/usr/bin/env bash
domain="$1";
function myarray(){
# the array function which will pass the domains one by one
}
function A(){
ping $domain -c 4
}
function B(){
host $domain
}
myarray;
A;
B;
Converting comments into an answer.
The natural way to write shell scripts is to pass the arguments separately:
./script.sh domain1.com domain2.com
and iterate over the elements of "$#" in the script, passing the domain names explicitly to the function (as shown) — global variables are just as problematic in shell scripts as in other programming languages.
#!/usr/bin/env bash
A() { ping "$1" -c 4; }
B() { host "$1"; }
for domain in "$#"
do
A $domain
B $domain
done
If you must use the comma-separated argument, then use:
domains=($(echo "$1" | tr "," "\n"))
pingme() { ping "$1" -c 4; }
for domain in "${domains[#]}"
do
pingme $domain
done
and (as before) the function should process $1, not a global variable.
Inside a function, $1 is the first argument passed to the function, not what's passed to the script as a whole.
Note that in the original code, none of the semicolons is needed. In the code I wrote, the semicolons before the close braces are needed because I lazily wrote the functions on a single line (because they are so simple — I would probably not use functions for such simple commands, especially as they're each only invoked once in the script). If they were spread over three or four lines, the semicolons would not be needed.
A()
{
ping "$1" -c 4
}
Also, you don't need to use the keyword function — and it is generally regarded as a bad idea to do so. As Charles Duffy noted in a comment, the Bash Hackers wiki indicates you should not use function.

How to use/call variables from another function in bash

I am learning to play around with functions in bash. I have the first function read_file() that reads /etc/file and replaces ':'with a space between words (e.g root:x:0:0:root ... becomes root x 0 0 root ... ). I then want to be able to manipulate output from individual words in each of the lines.
My second function- display__user_shell() prints our the shell for each corresponding users as is in the /etc/file.
My problem is figuring out how to call the first function read_file() and using its variables in the display__user_shell function.
I have been able to do the above when using input from a single line rather than reading from a file.
i just called new_data -i.e $new_data from the display__user_shell() function
read_file() {
read -p "Enter file" file
while read line
do
newlin=$(echo $line | tr ":" " ")
echo newlin
done
}
oldIFS=$IFS
IFS=" "
ct=0
display__user_shell() {
readfile
for item in $newlin;
do
[ $ct -eq 0 ] && name="$item";
[ $ct -eq 6 ] && name="$item";
done
echo "$user's shell is $shell"
}
IFS=$oldIFS
display__user_shell
the first line of the output should be..
root's shell is /bin/bash
Irrespective of the implementation there is an interesting question here: how to reference variables from one function in another function. The short answer is that you can:
$ a() { aye=bee; }
$ b() { echo "$aye"; }
$ a
$ b
bee
But this is a very bad idea - Bash has "unfortunate" scoping rules different from safer languages like Java, Python, or Ruby, and code like this is very hard to follow. Instead there are several patterns you can use to produce more readable code:
Print the value in the inner function and assign that to a value in the outer function:
a() {
echo 'bee'
}
b() {
aye="$(a)"
echo "$aye"
}
b # Prints "bee"
Call and assign to a variable the first function in the outer scope and use it in the second function:
a() {
echo 'bee'
}
aye="$(a)"
b() {
echo "$aye"
}
b # Prints "bee"
Treat the first and second functions as a pipeline, passing standard output of the first one to the standard input of the second one (read is a slow way to process a large file, but it'll serve as an example):
a() {
echo 'bee'
}
b() {
while read -r line
do
echo "$line"
done
}
a | b # Prints "bee"
Which one you choose depends on things like what else you intend to do with what a returns and whether a produces huge amounts of output.

Use variables outside the subprocess in bash

There's a getStrings() function that calls getPage() function that returns some html page. That html is piped through egrep and sed combination to get only 3 strings. Then I try to put every string into separate variable link, profile, gallery respectively using while read.. construction. But it works only inside the while...done loop because it runs in subprocess. What should I do to use those variables outside the getStrings() function?
getStrings() {
local i=2
local C=0
getPage $(getPageLink 1 $i) |
egrep *some expression that results in 3 strings* |
while read line; do
if (( (C % 3) == 0 )); then
link=$line
elif (( (C % 3) == 1 )); then
profile=$line
else
gallery=$line
fi
C=$((C+1)) #Counter
done
}
Simple: don't run the loop in a subprocess :)
To actually accomplish that, you can use process substitution.
while read line; do
...
done < <(getPage $(getPageLink 1 $i) | egrep ...)
For the curious, a POSIX-compatible way is to use a named pipe (and its possible that bash uses named pipes to implement process substitution):
mkfifo pipe
getPage $(getPageLink 1 $i) | egrep ... > pipe &
while read line; do
...
done < pipe
Starting in bash 4.2, you can just set the lastpipe option, which causes the last command in a pipeline to run in the current shell, rather than a subshell.
shopt -s lastpipe
getPage $(getPageLink 1 $i) | egrep ... | while read line; do
...
done
However, using a while loop is not the best way to set the three variables. It's easier to just call read three times within a command group, so that they all read from the same stream. In any of the three scenarios above, replace the while loop with
{ read link; read profile; read gallery; }
If you want to be a little more flexible, put the names of the variables you might want to read in an array:
fields=( link profile gallery )
then replace the while loop with this for loop instead:
for var in "${fields[#]}"; do read $var; done
This lets you easily adjust your code, should the pipeline ever return more or fewer lines, by just editing the fields array to have the appropriate field names.
One more solving using array:
getStrings() {
array_3=( `getPage | #some function
egrep | ...` ) #pipe conveyor
}

read stdin in function in bash script

I have some set of bash functions which output some information:
find-modelname-in-epson-ppds
find-modelname-in-samsung-ppds
find-modelname-in-hp-ppds
etc ...
I've been writing functions which read output and filter it:
function filter-epson {
find-modelname-in-epson-ppds | sed <bla-blah-blah>
}
function filter-hp {
find-modelname-in-hp-ppds | sed <the same bla-blah-blah>
}
etc ...
But the I thought that it would be better do something like this:
function filter-general {
(somehow get input) | sed <bla-blah-blah>
}
and then call in another high-level functions:
function high-level-func {
# outputs filtered information
find-modelname-in-hp/epson/...-ppds | filter-general
}
How can I achieve that with the best bash practices?
If the question is How do I pass stdin to a bash function?, then the answer is:
Shellscript functions take stdin the ordinary way, as if they were commands or programs. :)
input.txt:
HELLO WORLD
HELLO BOB
NO MATCH
test.sh:
#!/bin/sh
myfunction() {
grep HELLO
}
cat input.txt | myfunction
Output:
hobbes#metalbaby:~/scratch$ ./test.sh
HELLO WORLD
HELLO BOB
Note that command line arguments are ALSO handled in the ordinary way, like this:
test2.sh:
#!/bin/sh
myfunction() {
grep "$1"
}
cat input.txt | myfunction BOB
Output:
hobbes#metalbaby:~/scratch/$ ./test2.sh
HELLO BOB
To be painfully explicit that I'm piping from stdin, I sometimes write
cat - | ...
A very simple means to get stdin into a variable is to use read. By default, it reads file descriptor "0", i.e. stdin i.e., /dev/stdin.
Example Function:
input(){ local in; read in; echo you said $in; }
Example implementation:
echo "Hello World" | input
Result:
you said Hello World
Additional info
You don't need to declare a variable as being local, of course. I just included that for the sake of good form. Plain old read in does what you need.
So you understand how read works, by default it reads data off the given file descriptor (or implicit stdin) and blocks until it encounters a newline. Much of the time, you'll find that will implicitly be attached to your input, even if you weren't aware of it. If you have a function that seems to hang with this mechanism just keep this detail in mind (there are other ways of using read to deal with that...).
More robust solutions
Adding on to the basic example, here's a variation that lets you pass the input via a stdin OR an argument:
input()
{
local in=$1; if [ -z "$in" ]; then read in; fi
echo you said $in
}
With that tweak, you could ALSO call the function like:
input "Hello World"
How about handling an stdin option plus other arguments? Many standard nix utilities, especially those which typically work with stdin/stdout adhere to the common practice of treating a dash - to mean "default", which contextually means either stdin or stdout, so you can follow the convention, and treat an argument specified as - to mean "stdin":
input()
{
local a=$1; if [ "$a" == "-" ]; then read a; fi
local b=$2
echo you said $a $b
}
Call this like:
input "Hello" "World"
or
echo "Hello" | input - "World"
Going even further, there is actually no reason to only limit stdin to being an option for only the first argument! You might create a super flexible function that could use it for any of them...
input()
{
local a=$1; if [ "$a" == "-" ]; then read a; fi
local b=$2; if [ "$b" == "-" ]; then read b; fi
echo you said $a $b
}
Why would you want that? Because you could formulate, and pipe in, whatever argument you might need...
myFunc | input "Hello" -
In this case, I pipe in the 2nd argument using the results of myFunc rather than the only having the option for the first.
Call sed directly. That's it.
function filter-general {
sed <bla-blah-blah>
}

Create associative array in bash 3

After thoroughly searching for a way to create an associative array in bash, I found that declare -A array will do the trick. But the problem is, it is only for bash version 4 and the bash version the server has in our system is 3.2.16.
How can I achieve some sort of associative array-like hack in bash 3? The values will be passed to a script like
ARG=array[key];
./script.sh ${ARG}
EDIT: I know that I can do this in awk, or other tools but strict bash is needed for the scenario I am trying to solve.
Bash 3 has no associative arrays, so you're going to have to use some other language feature(s) for your purpose. Note that even under bash 4, the code you wrote doesn't do what you claim it does: ./script.sh ${ARG} does not pass the associative array to the child script, because ${ARG} expands to nothing when ARG is an associative array. You cannot pass an associative array to a child process, you need to encode it anyway.
You need to define some argument passing protocol between the parent script and the child script. A common one is to pass arguments in the form key=value. This assumes that the character = does not appear in keys.
You also need to figure out how to represent the associative array in the parent script and in the child script. They need not use the same representation.
A common method to represent an associative array is to use separate variables for each element, with a common naming prefix. This requires that the key name only consists of ASCII letters (of either case), digits and underscores. For example, instead of ${myarray[key]}, write ${myarray__key}. If the key is determined at run time, you need a round of expansion first: instead of ${myarray[$key]}, write
n=myarray__${key}; echo ${!n}
For an assignment, use printf -v. Note the %s format to printf to use the specified value. Do not write printf -v "myarray__${key}" %s "$value" since that would treat $value as a format and perform printf % expansion on it.
printf -v "myarray__${key}" %s "$value"
If you need to pass an associative array represented like this to a child process with the key=value argument representation, you can use ${!myarray__*} to enumerate over all the variables whose name begins with myarray__.
args=()
for k in ${!myarray__*}; do
n=$k
args+=("$k=${!n}")
done
In the child process, to convert arguments of the form key=value to separate variables with a prefix:
for x; do
if [[ $x != *=* ]]; then echo 1>&2 "KEY=VALUE expected, but got $x"; exit 120; fi
printf -v "myarray__${x%%=*}" %s "${x#*=}"
done
By the way, are you sure that this is what you need? Instead of calling a bash script from another bash script, you might want to run the child script in a subshell instead. That way it would inherit from all the variables of the parent.
Here is another post/explanation on associative arrays in bash 3 and older using parameter expansion:
https://stackoverflow.com/a/4444841
Gilles' method has a nice if statement to catch delimiter issues, sanitize oddball input ...etc. Use that.
If you are somewhat familiar with parameter expansion:
http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
To use in your scenario [ as stated: sending to script ]:
Script 1:
sending_array.sh
# A pretend Python dictionary with bash 3
ARRAY=( "cow:moo"
"dinosaur:roar"
"bird:chirp"
"bash:rock" )
bash ./receive_arr.sh "${ARRAY[#]}"
Script 2: receive_arr.sh
argAry1=("$#")
function process_arr () {
declare -a hash=("${!1}")
for animal in "${hash[#]}"; do
echo "Key: ${animal%%:*}"
echo "Value: ${animal#*:}"
done
}
process_arr argAry1[#]
exit 0
Method 2, sourcing the second script:
Script 1:
sending_array.sh
source ./receive_arr.sh
# A pretend Python dictionary with bash 3
ARRAY=( "cow:moo"
"dinosaur:roar"
"bird:chirp"
"bash:rock" )
process_arr ARRAY[#]
Script 2: receive_arr.sh
function process_arr () {
declare -a hash=("${!1}")
for animal in "${hash[#]}"; do
echo "Key: ${animal%%:*}"
echo "Value: ${animal#*:}"
done
}
References:
Passing arrays as parameters in bash
If you don't want to handle a lot of variables, or keys are simply invalid variable identifiers, and your array is guaranteed to have less than 256 items, you can abuse function return values. This solution does not require any subshell as the value is readily available as a variable, nor any iteration so that performance screams. Also it's very readable, almost like the Bash 4 version.
Here's the most basic version:
hash_index() {
case $1 in
'foo') return 0;;
'bar') return 1;;
'baz') return 2;;
esac
}
hash_vals=("foo_val"
"bar_val"
"baz_val");
hash_index "foo"
echo ${hash_vals[$?]}
More details and variants in this answer
You can write the key-value pairs to a file and then grep by key. If you use a pattern like
key=value
then you can egrep for ^key= which makes this pretty safe.
To "overwrite" a value, just append the new value at the end of the file and use tail -1 to get just the last result of egrep
Alternatively, you can put this information into a normal array using key=value as value for the array and then iterator over the array to find the value.
This turns out to be ridiculously easy. I had to convert a bash 4 script that used a bunch of associative arrays to bash 3. These two helper functions did it all:
array_exp() {
exp=${#//[/__}
eval "${exp//]}"
}
array_clear() {
unset $(array_exp "echo \${!$1__*}")
}
I'm flabbergasted that this actually works, but that's the beauty of bash.
E.g.
((all[ping_lo] += counts[ping_lo]))
becomes
array_exp '((all[ping_lo] += counts[ping_lo]))'
Or this print statement:
printf "%3d" ${counts[ping_lo]} >> $return
becomes
array_exp 'printf "%3d" ${counts[ping_lo]}' >> $return
The only syntax that changes is clearing. This:
counts=()
becomes
array_clear counts
and you're set. You could easily tell array_exp to recognize expressions like "=()" and handle them by rewriting them as array_clear expressions, but I prefer the simplicity of the above two functions.

Resources