Pass multiple arguments from a text file to a bash script - bash

I have a bash script that takes three arguments, and I have a text file with three columns. I'd like the script to take the the text file's first, second, and third columns as the first, second, and third arguments of the bash script.
I'm a beginner with shell scripting, and am running fairly simple programs where efficiency is not very important, so simpler fixes would be appreciated :)
Here's an example of what I'd like to do.
I have a text file called food.txt with the following text:
fruits apples bananas
vegetables kale broccoli
meats pork beef
desserts cake pie
And I have a menu.sh script like this
#!/bin/bash
arg_1=$1
arg_2=$2
arg_3=$3
for arg_1 in $(cat food.txt)
do
echo "${arg_1}:${arg_2},${arg_3}"
done
I would like menu.sh to output this:
fruits:apples,bananas
vegetables:kale,broccoli
meats:pork,beef
desserts:cake,pie
But instead it outputs this:
fruits:,
apples:,
bananas:,
vegetables:,
kale:,
...
Thanks in advance for the help!

The right way with bash's read function:
#!/bin/bash
while read -r arg_1 arg_2 arg_3; do
echo "${arg_1}:${arg_2},${arg_3}"
done < food.txt
https://www.gnu.org/software/bash/manual/bash.html#Bash-Builtins

You met bash word spliting.
Check bash FAQ 1
You need to use
while read -r a1 a2 a3; do
echo "$a1:$a2:$a3"
done < file

cat food.txt | while read line
do
header="$(echo "$line" | cut -f1 -d' ')"
fields="$(echo "$line" | cut -f2,3 -d' ' | tr ' ' ,)"
echo "$header:$fields"
done
If you ever add another field / column you can just change that 2,3 to be 2,3,4 or 2-4. If you ever have a variable number of fields per line, you can change it to 2-.

Another interesting (but not optimal) way to achieve this is using IFS var(Internal Field Separator). It's used within bash to tell how to separate fields, in this case, in a for loop:
#!/bin/bash
file=$(cat food.txt)
IFS_OLD=$IFS
IFS=$'\n'
for line in $file; do
name=$(echo "$line" | cut -d' ' -f1 )
element1=$(echo "$line" | cut -d' ' -f2)
element2=$(echo "$line" | cut -d' ' -f3)
echo "$name:$element1,$element2"
done
IFS=$IFS_OLD
Note that is important to backup and restore original IFS value. As I said, is not optimal, the other answers are better, but this still being another option

Alternatively you could do:
#!/bin/bash
IFS=$'\n'
for arg in $(cat food.txt); do
echo $arg | awk '{ print $1":"$2","$3 }'
done
or:
#!/bin/bash
awk '{ print $1":"$2","$3 }' food.txt

Related

Weird bash results using cut

I am trying to run this command:
./smstocurl SLASH2.911325850268888.911325850268896
smstocurl script:
#SLASH2.911325850268888.911325850268896
model=$(echo \&model=$1 | cut -d'.' -f 1)
echo $model
imea1=$(echo \&simImea1=$1 | cut -d'.' -f 2)
echo $imea1
imea2=$(echo \&simImea2=$1 | cut -d'.' -f 3)
echo $imea2
echo $model$imea1$imea2
Result Received
&model=SLASH2911325850268888911325850268896
Result Expected
&model=SLASH2&simImea1=911325850268888&simImea2=911325850268896
What am I missing here ?
You are cutting based on the dot .. In the first case your desired string contains the first string, the one containing &model, so then it is printed.
However, in the other cases you get the 2nd and 3rd blocks (-f2, -f3), so that the imea text gets cutted off.
Instead, I would use something like this:
while IFS="." read -r model imea1 imea2
do
printf "&model=%s&simImea1=%s&simImea2=%s\n" $model $imea1 $imea2
done <<< "$1"
Note the usage of printf and variables to have more control about what we are writing. Using a lot of escapes like in your echos can be risky.
Test
while IFS="." read -r model imea1 imea2; do printf "&model=%s&simImea1=%s&simImea2=%s\n" $model $imea1 $imea2
done <<< "SLASH2.911325850268888.911325850268896"
Returns:
&model=SLASH2&simImea1=911325850268888&simImea2=911325850268896
Alternatively, this sed makes it:
sed -r 's/^([^.]*)\.([^.]*)\.([^.]*)$/\&model=\1\&simImea1=\2\&simImea2=\3/' <<< "$1"
by catching each block of words separated by dots and printing back.
You can also use this way
Run:
./program SLASH2.911325850268888.911325850268896
Script:
#!/bin/bash
String=`echo $1 | sed "s/\./\&simImea1=/"`
String=`echo $String | sed "s/\./\&simImea2=/"`
echo "&model=$String
Output:
&model=SLASH2&simImea1=911325850268888&simImea2=911325850268896
awk way
awk -F. '{print "&model="$1"&simImea1="$2"&simImea2="$3}' <<< "SLASH2.911325850268888.911325850268896"
or
awk -F. '$0="&model="$1"&simImea1="$2"&simImea2="$3' <<< "SLASH2.911325850268888.911325850268896"
output
&model=SLASH2&simImea1=911325850268888&simImea2=911325850268896

Using values of variables in on-the-spot shell commands (using ``)

I have a shell script that for-loops over input to get a number and string. If I want to test the number in the loop, can I cut the looped-over variable to get the number? For example, something like:
for line in input
do
num=`cut -f1 $line`
...
done
If not, how else can I accomplish this?
Instead of:
num=`cut -f1 $line`
You can do:
num=$(echo "$line" | cut -f1)
OR else using awk:
num=$(awk '{print $1}' <<< $line)
OR using pure BASH:
num=${line%% *}
Your command cut -f1 $line will try to cut first column from a file named as $line.
Is this what you want instead ?
while read -r number str
do
echo $number;
echo $str;
done < input

Is there a better way to retrieve the elements of a delimited pair in bash?

I have entries of the form: cat:rat and I would like to assign them to separate variables in bash. I am currently able to do this via:
A=$(echo $PAIR | tr ':' '\n' | head -n1)
B=$(echo $PAIR | tr ':' '\n' | tail -n1)
after which $A and $B are, respectively, cat and rat. echo, the two pipes and all feels a bit like overkill am I missing a much simpler way of doing this?
Using the read command
entry=cat:rat
IFS=: read A B <<< "$entry"
echo $A # => cat
echo $B # => rat
Yes using bash parameter substitution
PAIR='cat:rat'
A=${PAIR/:*/}
B=${PAIR/*:/}
echo $A
cat
echo $B
rat
Alternately, if you are willing to use an array in place of individual variables:
IFS=: read -r -a ARR <<<"${PAIR}"
echo ${ARR[0]}
cat
echo ${ARR[1]}
rat
EDIT: Refer glenn jackman's answer for the most elegant read-based solution
animal="cat:rat"
A=echo ${animal} | cut -d ":" -f1
B=echo ${animal} | cut -d ":" -f2
might not be the best solution. Just giving you a possible solution

Shell script: how to read only a portion of text from a variable

I'm developing a little script using ash shell (not bash).
Now i have a variable with the following composition:
VARIABLE = "number string status"
where number could be any number (actually between 1 and 18 but in the future that number could be higher) the string is a name and status is or on or off
The name usually is only lowercase letter.
Now my problem is to read only the string content in the variable, removing the number and the status.
How i can obtain that?
Two ways; one is to leverage $IFS and use a while loop - this will work for a single line quite happily - as:
echo "Part1 Part2 Part3" | while read a b c
do
echo $a
done
alternatively, use cut as follows:
a=`echo $var | cut -d' ' -f2`
echo $a
How about using cut?
name=$(echo "$variable" | cut -d " " -f 2)
UPDATE
Apparently, Ash doesn't understand $(...). Hopefully you can do this instead:
name=`echo "$variable" | cut -d " " -f 2`
How about :
name=$(echo "$variable" | awk '{print $2}')
#!/bin/sh
myvar="word1 word2 word3 wordX"
set -- $myvar
echo ${15} # outputs word 15

results of wc as variables

I would like to use the lines coming from 'wc' as variables. For example:
echo 'foo bar' > file.txt
echo 'blah blah blah' >> file.txt
wc file.txt
2 5 23 file.txt
I would like to have something like $lines, $words and $characters associated to the values 2, 5, and 23. How can I do that in bash?
In pure bash: (no awk)
a=($(wc file.txt))
lines=${a[0]}
words=${a[1]}
chars=${a[2]}
This works by using bash's arrays. a=(1 2 3) creates an array with elements 1, 2 and 3. We can then access separate elements with the ${a[indice]} syntax.
Alternative: (based on gonvaled solution)
read lines words chars <<< $(wc x)
Or in sh:
a=$(wc file.txt)
lines=$(echo $a|cut -d' ' -f1)
words=$(echo $a|cut -d' ' -f2)
chars=$(echo $a|cut -d' ' -f3)
There are other solutions but a simple one which I usually use is to put the output of wc in a temporary file, and then read from there:
wc file.txt > xxx
read lines words characters filename < xxx
echo "lines=$lines words=$words characters=$characters filename=$filename"
lines=2 words=5 characters=23 filename=file.txt
The advantage of this method is that you do not need to create several awk processes, one for each variable. The disadvantage is that you need a temporary file, which you should delete afterwards.
Be careful: this does not work:
wc file.txt | read lines words characters filename
The problem is that piping to read creates another process, and the variables are updated there, so they are not accessible in the calling shell.
Edit: adding solution by arnaud576875:
read lines words chars filename <<< $(wc x)
Works without writing to a file (and do not have pipe problem). It is bash specific.
From the bash manual:
Here Strings
A variant of here documents, the format is:
<<<word
The word is expanded and supplied to the command on its standard input.
The key is the "word is expanded" bit.
lines=`wc file.txt | awk '{print $1}'`
words=`wc file.txt | awk '{print $2}'`
...
you can also store the wc result somewhere first.. and then parse it.. if you're picky about performance :)
Just to add another variant --
set -- `wc file.txt`
chars=$1
words=$2
lines=$3
This obviously clobbers $* and related variables. Unlike some of the other solutions here, it is portable to other Bourne shells.
I wanted to store the number of csv file in a variable. The following worked for me:
CSV_COUNT=$(ls ./pathToSubdirectory | grep ".csv" | wc -l | xargs)
xargs removes the whitespace from the wc command
I ran this bash script not in the same folder as the csv files. Thus, the pathToSubdirectory
You can assign output to a variable by opening a sub shell:
$ x=$(wc some-file)
$ echo $x
1 6 60 some-file
Now, in order to get the separate variables, the simplest option is to use awk:
$ x=$(wc some-file | awk '{print $1}')
$ echo $x
1
declare -a result
result=( $(wc < file.txt) )
lines=${result[0]}
words=${result[1]}
characters=${result[2]}
echo "Lines: $lines, Words: $words, Characters: $characters"

Resources