how to assign variable to each value in quotes - linux

Is there a way to assign multiple variables from following string?
edit "admin1" edit "admin2" edit "admin3"
I would like to have
var1=admin1 var2=admin2 var3=admin3
Thanks.

Try this:
read -r trash var1 trash var2 trash var3 <<< $(echo edit "admin1" edit "admin2" edit "admin3")

Would you please try the following:
#!/bin/bash
str='edit "admin1" edit "admin2" edit "admin3"' # input string
pat='[^"]*"([^"]*)"(.*)' # regex to match the string
while [[ $str =~ $pat ]]; do # match the regex
printf -v 'var'$((++n)) "${BASH_REMATCH[1]}"
# assign varN to the matched substring
str="${BASH_REMATCH[2]}" # update "str" by truncating left to right
done <<< "$str"
# see the results
echo "$var1"
echo "$var2"
echo "$var3"
The advantage of the script above is you don't need to know the number of variables in advance.
Explanation of the regex pat:
[^"]* matches a sequence of zero or more any characters other
than double quotes. It works to skip extra strings such as edit.
"([^"]*)" matches a string enclosed with the double quotes.
Bash variable ${BASH_REMATCH[1]} is automatically assigned to the enclosed string, as the pattern is wrapped with parentheses.
(.*) matches the remaining substring. The variable str is re-assigned
to it (being truncated) to be used in the next while loop.
The while loop keeps matching regex pat with str until there is no match.
Then the statement printf -v 'var'$((++n)) "${BASH_REMATCH[1]}" generates a variable
var1, var2, var3, .. assigning them to the strings
enclosed with the double quotes in order.
Here is an illustration how the regex works in the loop:
1st loop:
edit "admin1" edit "admin2" edit "admin3"
<---> <----> <-------------------------->
skipped | |
BASH_REMATCH[1] BASH_REMATCH[2]
var1 := BASH_REMATCH[1]
str := BASH_REMATCH[2]
2nd loop:
edit "admin2" edit "admin3"
<----> <----> <------------>
skipped | |
BASH_REMATCH[1] BASH_REMATCH[2]
var2 := BASH_REMATCH[1]
str := BASH_REMATCH[2]
3rd loop:
edit "admin3"
<----> <---->
skipped |
BASH_REMATCH[1]
var3 := BASH_REMATCH[1]
str := empty

There are a lot of ways to do this, but it really depends on how variable your data is, and how reliably you can parse your inputs. Given your example above, the following here-string and process substitution with the read builtin will work:
read var1 var2 var3 < \
<(sed -r 's/edit ?//g' <<< 'edit "admin1" edit "admin2" edit "admin3"')
echo $var1; echo $var2; echo $var3
This will correctly print:
"admin1"
"admin2"
"admin3"
You can also collect input as indexed or associative arrays, or use indirect variable expansions. However, if your real-world inputs are more complicated, your biggest challenge will be scanning for only the words in quotes, and will likely involve a lot of splitting and looping in one form or another.

This Shellcheck-clean demonstration program uses the built-in regular expression support in Bash:
#! /bin/bash -p
edit_rx='edit[[:space:]]+"([^"]*)"'
all_edits_rx="${edit_rx}[[:space:]]+${edit_rx}[[:space:]]+${edit_rx}"
teststr='edit "admin1" edit "admin2" edit "admin3"'
if [[ $teststr =~ $all_edits_rx ]]; then
var1=${BASH_REMATCH[1]}
var2=${BASH_REMATCH[2]}
var3=${BASH_REMATCH[3]}
else
printf "ERROR: '%s' does not match '%s'\\n" "$all_edits_rx" "$teststr" >&2
exit 1
fi
declare -p var1 var2 var3
An advantage of this approach is that it does a thorough check that the input has the expected format, thus minimizing the risk of Garbage in, garbage out.
If the number of quoted values in the string is not always 3 (and maybe even if it is always 3) it would be better to put the values in an array rather than in numbered variables. This Shellcheck-clean demonstration program shows one way to do it:
#! /bin/bash -p
editlist_rx='^edit[[:space:]]+"([^"]*)"[[:space:]]*(.*)$'
teststr='edit "admin1" edit "admin2" edit "admin3" edit "admin4" edit "admin5"'
tmp=$teststr
vars=()
while [[ $tmp =~ $editlist_rx ]]; do
vars+=( "${BASH_REMATCH[1]}" )
tmp=${BASH_REMATCH[2]}
done
if [[ -n $tmp ]]; then
printf "ERROR: failed match at '%s'\\n" "$tmp" >&2
exit 1
fi
for i in "${!vars[#]}"; do
printf 'vars[%d]=%q\n' "$i" "${vars[i]}"
done
It outputs:
vars[0]=admin1
vars[1]=admin2
vars[2]=admin3
vars[3]=admin4
vars[4]=admin5

Related

How to test for certain characters in a file

I am currently running a script with an if statement. Before I run the script, I want to make sure the file provided as the first argument has certain characters.
If the file does not have those certain characters in certain spots then the output would be else "File is Invalid" on the command line.
For the if statement to be true, the file needs to have at least one hyphen in Field 1 line 1 and at least one comma in Field one Line one.
How would I create an if statement with perhaps a test command to validate those certain characters are present?
Thanks
Im new to Linux/Unix, this is my homework so I haven't really tried anything, only brain storming possible solutions.
function usage
{
echo "usage: $0 filename ..."
echo "ERROR: $1"
}
if [ $# -eq 0 ]
then
usage "Please enter a filename"
else
name="Yaroslav Yasinskiy"
echo $name
date
while [ $# -gt 0 ]
do
if [ -f $1 ]
then
if <--------- here is where the answer would be
starting_data=$1
echo
echo $1
cut -f3 -d, $1 > first
cut -f2 -d, $1 > last
cut -f1 -d, $1 > id
sed 's/$/:/' last > last1
sed '/last:/ d' last1 > last2
sed 's/^ *//' last2 > last3
sed '/first/ d' first > first1
sed 's/^ *//' first1 > first2
sed '/id/ d' id > id1
sed 's/-//g' id1 > id2
paste -d\ first2 last3 id2 > final
cat final
echo ''
else
echo
usage "Coult not find file $1"
fi
shift
done
fi
In answer to your direct question:
For the if statement to be true, the file needs to have at least one
hyphen in Field 1 line 1 and at least one comma in Field one Line one.
How would I create an if statement with perhaps a test command to
validate those certain characters are present?
Bash provides all the tools you need. While you can call awk, you really just need to read the first line of the file into two-variable (say a and b) and then use the [[ $a =~ regex ]] to where the regex is an extended regular expression that verifies that the first field (contained in $a) contains both a '-' and ','.
For details on the [[ =~ ]] expression, see bash(1) - Linux manual page under the section labeled [[ expression ]].
Let's start with read. When you provide two variables, read will read the first field (based on normal word-splitting given by IFS (the Internal Field Separator, default $'[ \t\n]' - space, tab, newline)). So by doing read -r a b you read the first field into a and the rest of the line into b (you don't care about b for your test)
Your regex can be ([-]+.*[,]+|[,]+.*[-]+) which is an (x|y), e.g. x OR y expression where x is [-]+.*[,]+ (one or more '-' and one or more ','), your y is [,]+.*[-]+ (one or more ',' and one or more '-'). So by using the '|' your regex will accept either a comma then zero-or-more characters and a hyphen or a hyphen and zero-or-more characters and then a comma in the first field.
How do you read the line? With simple redirection, e.g.
read -r a b < "$1"
So your conditional test in your script would look something like:
if [ -f $1 ]
then
read -r a b < "$1"
if [[ $a =~ ([-]+.*[,]+|[,]+.*[-]+) ]] # <-- here is where the ...
then
starting_data=$1
...
else
echo "File is Invalid" >&2 # redirection to 2 (stderr)
fi
else
echo
usage "Coult not find file $1"
fi
shift
...
Example Test Files
$ cat valid
dog-food, cat-food, rabbit-food
50lb 16lb 5lb
$ cat invalid
dogfood, catfood, rabbitfood
50lb 16lb 5lb
Example Use/Output
$ read -r a b < valid
if [[ $a =~ ([-]+.*[,]+|[,]+.*[-]+) ]]; then
echo "file valid"
else
echo "file invalid"
fi
file valid
and for the file without the certain characters:
$ read -r a b < invalid
if [[ $a =~ ([-]+.*[,]+|[,]+.*[-]+) ]]; then
echo "file valid"
else
echo "file invalid"
fi
file invalid
Now you really have to concentrate on eliminating the spawning of at least a dozen subshells where you call cut 3-times, sed 7-times, paste once and then cat. While it is good you are thinking through what you need to do, and getting it working, as mentioned in my comment, any time you are looping, you want to eliminate the number of subshells spawned to the greatest extent possible. I suspect as #Mig answered, awk will be the proper tool that can likely eliminate all 12 subshells are replace it with a single call to awk.
I personally would use awk for this all part since you want to test fields and create a string with concatenated fields. Awk is perfect for that.
But here is a small script which shows how you could just test your file's first line:
if [[ $(head -n 1 file.csv | awk '$1~/-/ && $1~/,/ {print "MATCH"}') == 'MATCH' ]]; then
echo "yes"
else
echo "no"
fi
It looks overkill when not doing the whole thing in awk but it works. I am sure there is a way to test only one regex, but that would involve knowing which flavour of awk you have because I think they don't all use the same regex engine. Therefore I left this out for the sake of simplicity.

Decrement variables that contain letters

I have a set of valid characters [0-9a-zA-Z] and a variable that is assigned one of these characters. What I want to do is to be able to decrement that variable to the next in the set.
I can't figure out how to decrement letters , it works for numbers only.
#!/bin/bash
test=b
echo $test # this shows 'b'
let test-=1
echo $test # I want it to be 'a'
The advantage of
test=$(tr 1-9a-zA-Z 0-9a-zA-Y <<<"$test")
is that it correctly (I think) decrements a to 9 and A to z. And if that is not the order you want, it is easy to adjust.
See man tr for details. This is the Gnu version of tr; character ranges are not guaranteed by Posix, but most tr implementations have them. <<< "here strings" are also a common extension, which bash implements.
test=$(printf "\\$(printf '%03o' "$(($(printf '%d' "'$test") - 1 ))")")
you could try this:
#!/bin/bash
test=b
if [[ $test == A || $test == a || $test == 0 ]]
then
echo "character already at lowest value"
else
# convert $test to decimal digit
test_digit=$(printf '%d' "'$test")
decremented=$(( test_digit - 1 ))
# print $decremented as a char
printf "\\$(printf '%03o' "$decremented")\n"
fi
reference:
http://mywiki.wooledge.org/BashFAQ/071
If we set a variable (say a) to the whole string of characters:
$ a=$( IFS=''; set -- {0..9} {a..z} {A..Z}; echo "$*"); echo "$a"
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
We may take advantage of the fact that bash "arithmetic" may use a base up to 62 (in the same order as the letters presented).
$ test="A"; echo "${a:$((62#$test-1)):1}"
z
This works only for "one character" (and not zero 0).
It may be expanded to several characters, but that is not being asked.

reading a variable and searching if and then statement to match something in a variable

Hi I Have a command/function is a script that assigns users names to variable
so:
var1=dedwards torontobgs NJVMTTS2 srounce jstickler
echo $var1 produces >> dedwards torontobgs NJVMTTS2 srounce jstickler
How can I write a command to see if the userid "NJVMTTS2" exist in the variable ? It seems that variable is treated as one string and not individual string?
I know this is simple but cannot get it to work
What I want to do is as follow:
if $1 ( passed by anther function ) exist in $VAR1
then do something
so if $1 = NJVMTTS2
and if NJVMTTS2 contained in the $VAR1
Then do echo "yes" else echo "no"
Just standard pattern matching.
var1='dedwards torontobgs NJVMTTS2 srounce jstickler'
if [[ $var1 = *NJVMTTS2* ]]; then
…
fi
Or, for portability:
case "$var1" in
*NJVMTTS2*) …;;
esac
Update
if NJVMTTS2 is in a parameter, you should quote the parameter expansion, but you must be careful not to quote the pattern:
case "$var1" in
*"$1"*) …;;
esac
First, your assignment should be quoted, otherwise var1 will contain only the first word.
var1="dedwards torontobgs NJVMTTS2 srounce jstickler"
Then you can grep for the name inside the variable:
if echo $var1 | grep '\bNJVMTTS2\b' >/dev/null; then echo Found it; fi
The \b word boundary symbols make sure you look for the whole word, thus matching NJVMTTS2 but not fooNJVMTTS2bar
If fooNJVMTTS2bar is also OK, the other answer will work. #Kojiro's solution also has the advantage that it does not use an external program, it's pure shell.

Bash reading txt file and storing in array

I'm writing my first Bash script, I have some experience with C and C# so I think the logic of the program is correct, it's just the syntax is so complicated because apparently there are many different ways to write the same thing!
Here is the script, it simply checks if the argument (string) is contained in a certain file. If so it stores each line of the file in an array and writes an item of the array in a file. I'm sure there must be easier ways to achieve that but I want to do some practice with bash loops
#!/bin/bash
NOME=$1
c=0
#IF NAME IS FOUND IN THE PHONEBOOK THEN STORE EACH LINE OF THE FILE INTO ARRAY
#ONCE THE ARRAY IS DONE GET THE INDEX OF MATCHING NAME AND RETURN ARRAY[INDEX+1]
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY"
while read line
do
myArray[$c]=$line # store line
c=$(expr $c + 1) # increase counter by 1
done < /root/phonebook.txt
else
echo "Name not found"
fi
c=0
for i in myArray;
do
if myArray[$i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
This code returns the only the second item of myArray (myArray[2]) or the second line of the file, why?
The first part (where you build the array) looks ok, but the second part has a couple of serious errors:
for i in myArray; -- this executes the loop once, with $i set to "myArray". In this case, you want $i to iterate over the indexes of myArray, so you need to use
for i in "${!myArray[#]}"
or
for ((i=0; i<${#a[#]}; i++))
(although I generally prefer the first, since it'll work with noncontiguous and associative arrays).
Also, you don't need the ; unless do is on the same line (in shell, ; is mostly equivalent to a line break so having a semicolon at the end of a line is redundant).
if myArray[$i]="$NOME" ; then -- the if statement takes a command, and will therefore treat myArray[$i]="$NOME" as an assignment command, which is not at all what you wanted. In order to compare strings, you could use the test command or its synonym [
if [ "${myArray[i]}" = "$NOME" ]; then
or a bash conditional expression
if [[ "${myArray[i]}" = "$NOME" ]]; then
The two are very similar, but the conditional expression has much cleaner syntax (e.g. in a test command, > redirects output, while \> is a string comparison; in [[ ]] a plain > is a comparison).
In either case, you need to use an appropriate $ expression for myArray, or it'll be interpreted as a literal. On the other hand, you don't need a $ before the i in "${myArray[i]}" because it's in a numeric expression context and therefore will be expanded automatically.
Finally, note that the spaces between elements are absolutely required -- in shell, spaces are very important delimiters, not just there for readability like they usually are in c.
1.-This is what you wrote with small adjustments
#!/bin/bash
NOME=$1
#IF NAME IS FOUND IN THE PHONE-BOOK **THEN** READ THE PHONE BOOK LINES INTO AN ARRAY VARIABLE
#ONCE THE ARRAY IS COMPLETED, GET THE INDEX OF MATCHING LINE AND RETURN ARRAY[INDEX+1]
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
IFS= while read -r line #IFS= in case you want to preserve leading and trailing spaces
do
myArray[c]=$line # put line in the array
c=$((c+1)) # increase counter by 1
done < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
fi
done
else
echo "Name not found"
fi
2.-But you can also read the array and stop looping like this:
#!/bin/bash
NOME=$1
c=0
if grep "$NOME" /root/phonebook.txt ; then
echo "CREATING ARRAY...."
readarray myArray < /root/phonebook.txt
for i in ${!myArray[#]}; do
if myArray[i]="$NOME" ; then
echo ${myArray[i+1]} >> /root/numbertocall.txt
break # stop looping
fi
done
else
echo "Name not found"
fi
exit 0
3.- The following improves things. Supposing a)$NAME matches the whole line that contains it and b)there's always one line after a $NOME found, this will work; if not (if $NOME can be the last line in the phone-book), then you need to do small adjustments.
!/bin/bash
PHONEBOOK="/root/phonebook.txt"
NUMBERTOCALL="/root/numbertocall.txt"
NOME="$1"
myline=""
myline=$(grep -A1 "$NOME" "$PHONEBOOK" | sed '1d')
if [ -z "$myline" ]; then
echo "Name not found :-("
else
echo -n "$NOME FOUND.... "
echo "$myline" >> "$NUMBERTOCALL"
echo " .... AND SAVED! :-)"
fi
exit 0

How to detect spaces in shell script variable [duplicate]

This question already has answers here:
How to check if a string has spaces in Bash shell
(10 answers)
Closed 3 years ago.
e.g string = "test test test"
I want after finding any occurance of space in string, it should echo error and exit else process.
The case statement is useful in these kind of cases:
case "$string" in
*[[:space:]]*)
echo "argument contains a space" >&2
exit 1
;;
esac
Handles leading/trailing spaces.
There is more than one way to do that; using parameter expansion
you could write something like:
if [ "$string" != "${string% *}" ]; then
echo "$string contains one or more spaces";
fi
For a purely Bash solution:
function assertNoSpaces {
if [[ "$1" != "${1/ /}" ]]
then
echo "YOUR ERROR MESSAGE" >&2
exit 1
fi
}
string1="askdjhaaskldjasd"
string2="asjkld askldja skd"
assertNoSpaces "$string1"
assertNoSpaces "$string2" # will trigger error
"${1/ /}" removes any spaces in the input string, and when compared to the original string should be exactly the same if there are not spaces.
Note the quotes around "${1/ /}" - This ensures that leading/trailing spaces are taken into consideration.
To match more than one character, you can use regular expressions to define a pattern to match - "${1/[ \\.]/}".
update
A better approach would be to use in-process expression matching. It will probably be a wee bit faster as no string manipulation is done.
function assertNoSpaces {
if [[ "$1" =~ '[\. ]' ]]
then
echo "YOUR ERROR MESSAGE" >&2
exit 1
fi
}
For more details on the =~ operator, see the this page and this chapter in the Advanced Bash Scripting guide.
The operator was introduced in Bash version 3 so watch out if you're using an older version of Bash.
update 2
Regarding question in comments:
how to handle the code if user enter
like "asd\" means in double quotes
...can we handle it??
The function given above should work with any string so it would be down to how you get input from your user.
Assuming you're using the read command to get user input, one thing you need to watch out for is that by default backslash is treated as an escape character so it will not behave as you might expect. e.g.
read str # user enters "abc\"
echo $str # prints out "abc", not "abc\"
assertNoSpaces "$str" # no error since backslash not in variable
To counter this, use the -r option to treat backslash as a standard character. See read MAN Page for details.
read -r str # user enters "abc\"
echo $str # prints out "abc\"
assertNoSpaces "$str" # triggers error
The == operator inside double brackets can match wildcards.
if [[ $string == *' '* ]]
You can use grep as:
string="test test test"
if ( echo "$string" | grep -q ' ' ); then
echo 'var has space'
exit 1
fi
I just ran into a very similar problem while handling paths. I chose to rely on my shell's parameter expansion rather than looking for a space specifically. It does not detect spaces at the front or the end, though.
function space_exit {
if [ $# -gt 1 ]
then
echo "I cannot handle spaces." 2>&1
exit 1
fi
}

Resources