How to get first character of variable - bash

I'm trying to get the first character of a variable, but I'm getting a Bad substitution error. Can anyone help me fix it?
code is:
while IFS=$'\n' read line
do
if [ ! ${line:0:1} == "#"] # Error on this line
then
eval echo "$line"
eval createSymlink $line
fi
done < /some/file.txt
Am I doing something wrong or is there a better way of doing this?
-- EDIT --
As requested - here's some sample input which is stored in /some/file.txt
$MOZ_HOME/mobile/android/chrome/content/browser.js
$MOZ_HOME/mobile/android/locales/en-US/chrome/browser.properties
$MOZ_HOME/mobile/android/components/ContentPermissionPrompt.js

To get the first character of a variable you need to say:
v="hello"
$ echo "${v:0:1}"
h
However, your code has a syntax error:
[ ! ${line:0:1} == "#"]
# ^-- missing space
So this can do the trick:
$ a="123456"
$ [ ! "${a:0:1}" == "#" ] && echo "doesnt start with #"
doesnt start with #
$ a="#123456"
$ [ ! "${a:0:1}" == "#" ] && echo "doesnt start with #"
$
Also it can be done like this:
$ a="#123456"
$ [ "$(expr substr $a 1 1)" != "#" ] && echo "does not start with #"
$
$ a="123456"
$ [ "$(expr substr $a 1 1)" != "#" ] && echo "does not start with #"
does not start with #
Update
Based on your update, this works to me:
while IFS=$'\n' read line
do
echo $line
if [ ! "${line:0:1}" == "#" ] # Error on this line
then
eval echo "$line"
eval createSymlink $line
fi
done < file

Adding the missing space (as suggested in fedorqui's answer ;) ) works for me.
An alternative method/syntax
Here's what I would do in Bash if I want to check the first character of a string
if [[ $line != "#"* ]]
On the right hand side of ==, the quoted part is treated literally whereas * is a wildcard for any sequence of character.
For more information, see the last part of Conditional Constructs of Bash reference manual:
When the ‘==’ and ‘!=’ operators are used, the string to the right of the operator is considered a pattern and matched according to the rules described below in Pattern Matching
Checking that you're using the right shell
If you are getting errors such as "Bad substitution error" and "[[: not found" (see comment) even though your syntax is fine (and works fine for others), it might indicate that you are using the wrong shell (i.e. not Bash).
So to make sure you are using Bash to run the script, either
make the script executable and use an appropriate shebang e.g. #!/bin/bash
or execute it via bash my_script
Also note that sh is not necessarily bash, sometimes it can be dash (e.g. in Ubuntu) or just plain ol' Bourne shell.

Try this:
while IFS=$'\n' read line
do
if ! [ "${line:0:1}" = "#" ]; then
eval echo "$line"
eval createSymlink $line
fi
done < /some/file.txt
or you can use the following for your if syntax:
if [[ ! ${line:0:1} == "#" ]]; then

TIMTOWTDI ^^
while IFS='' read -r line
do
case "${line}" in
"#"*) echo "${line}"
;;
*) createSymlink ${line}
;;
esac
done < /some/file.txt
Note: I dropped the eval, which could be needed in some (rare!) cases (and are dangerous usually).
Note2: I added a "safer" IFS & read (-r, raw) but you can revert to your own if it is better suited. Note that it still reads line by line.
Note3: I took the habit of using always ${var} instead of $var ... works for me (easy to find out vars in complex text, and easy to see where they begin and end at all times) but not necessary here.
Note4: you can also change the test to : *"#"*) if some of the (comments?) lines can have spaces or tabs before the '#' (and none of the symlink lines does contain a '#')

Related

Bash script with multiline variable

Here is my code
vmname="$1"
EXCEPTLIST="desktop-01|desktop-02|desktop-03|desktop-04"
if [[ $vmname != #(${EXCEPTLIST}) ]]; then
echo "${vmname}"
else
echo "Its in the exceptlist"
fi
The above code works perfectly but my question is , the EXCEPTLIST can be a long line, say 100 server names. In that case its hard to put all that names in one line. In that situation is there any way to make the variable EXCEPTLIST to be a multiline variable ? something like as follows:
EXCEPTLIST="desktop-01|desktop-02|desktop-03| \n
desktop-04|desktop-05|desktop-06| \n
desktop-07|desktop-08"
I am not sure but was thinking of possibilities.
Apparently I would like to know the terminology of using #(${})- Is this called variable expansion or what ? Does anyone know the documentation/explain to me about how this works in bash. ?
One can declare an array if the data/string is long/large. Use IFS and printf for the format string, something like:
#!/usr/bin/env bash
exceptlist=(
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
)
pattern=$(IFS='|'; printf '#(%s)' "${exceptlist[*]}")
[[ "$vmname" != $pattern ]] && echo good
In that situation is there any way to make the variable EXCEPTLIST to be a multiline variable ?
With your given input/data an array is also a best option, something like:
exceptlist=(
'desktop-01|desktop-02|desktop-03'
'desktop-04|desktop-05|desktop-06'
'desktop-07|desktop-08'
)
Check what is the value of $pattern variable one way is:
declare -p pattern
Output:
declare -- pattern="#(desktop-01|desktop-02|desktop-03|desktop-04|desktop-05|desktop-06)"
Need to test/check if $vmname is an empty string too, since it will always be true.
On a side note, don't use all upper case variables for purely internal purposes.
The $(...) is called Command Substitution.
See LESS=+'/\ *Command Substitution' man bash
In addition to what was mentioned in the comments about pattern matching
See LESS=+/'(pattern-list)' man bash
See LESS=+/' *\[\[ expression' man bash
s there any way to make the variable EXCEPTLIST to be a multiline variable ?
I see no reason to use matching. Use a bash array and just compare.
exceptlist=(
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
)
is_in_list() {
local i
for i in "${#:2}"; do
if [[ "$1" = "$i" ]]; then
return 0
fi
done
return 1
}
if is_in_list "$vmname" "${EXCEPTLIST[#]}"; then
echo "is in exception list ${vmname}"
fi
#(${})- Is this called variable expansion or what ? Does anyone know the documentation/explain to me about how this works in bash. ?
${var} is a variable expansion.
#(...) are just characters # ( ).
From man bash in Compund commands:
[[ expression ]]
When the == and != operators are used, the string to the right of the operator is considered a pattern and matched according to the rules
described below under Pattern Matching, as if the extglob shell option were enabled. ...
From Pattern Matching in man bash:
#(pattern-list)
Matches one of the given patterns
[[ command receives the #(a|b|c) string and then matches the arguments.
There is absolutely no need to use Bash specific regex or arrays and loop for a match, if using grep for raw string on word boundary.
The exception list can be multi-line, it will work as well:
#!/usr/bin/sh
exceptlist='
desktop-01|desktop-02|desktop-03|
deskop-04|desktop-05|desktop-06|
desktop-07|deskop-08'
if printf %s "$exceptlist" | grep -qwF "$1"; then
printf '%s is in the exceptlist\n' "$1"
fi
I wouldn't bother with multiple lines of text. This is would be just fine:
EXCEPTLIST='desktop-01|desktop-02|desktop-03|'
EXCEPTLIST+='desktop-04|desktop-05|desktop-06|'
EXCEPTLIST+='desktop-07|desktop-08'
The #(...) construct is called extended globbing pattern and what it does is an extension of what you probably already know -- wildcards:
VAR='foobar'
if [[ "$VAR" == fo?b* ]]; then
echo "Yes!"
else
echo "No!"
fi
A quick walkthrough on extended globbing examples: https://www.linuxjournal.com/content/bash-extended-globbing
#!/bin/bash
set +o posix
shopt -s extglob
vmname=$1
EXCEPTLIST=(
desktop-01 desktop-02 desktop-03
...
)
if IFS='|' eval '[[ ${vmname} == #(${EXCEPTLIST[*]}) ]]'; then
...
Here's one way to load a multiline string into a variable:
fn() {
cat <<EOF
desktop-01|desktop-02|desktop-03|
desktop-04|desktop-05|desktop-06|
desktop-07|desktop-08
EOF
}
exceptlist="$(fn)"
echo $exceptlist
As to solving your specific problem, I can think of a variety of approaches.
Solution 1, since all the desktop has the same desktop-0 prefix and only differ in the last letter, we can make use of {,} or {..} expansion as follows:
vmname="$1"
found=0
for d in desktop-{01..08}
do
if [[ "$vmname" == $d ]]; then
echo "It's in the exceptlist"
found=1
break
fi
done
if (( !found )); then
echo "Not found"
fi
Solution 2, sometimes, it is good to provide a list in a maintainable clear text list. We can use a while loop and iterate through the list
vmname="$1"
found=0
while IFS= read -r d
do
if [[ "$vmname" == $d ]]; then
echo "It's in the exceptlist"
found=1
break
fi
done <<EOF
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
desktop-07
desktop-08
EOF
if (( !found )); then
echo "Not found"
fi
Solution 3, we can desktop the servers using regular expressions:
vmname="$1"
if [[ "$vmname" =~ ^desktop-0[1-8]$ ]]; then
echo "It's in the exceptlist"
else
echo "Not found"
fi
Solution 4, we populate an array, then iterate through an array:
vmname="$1"
exceptlist=()
exceptlist+=(desktop-01 desktop-02 desktop-03 deskop-04)
exceptlist+=(desktop-05 desktop-06 desktop-07 deskop-08)
found=0
for d in ${exceptlist[#]}
do
if [[ "$vmname" == "$d" ]]; then
echo "It's in the exceptlist"
found=1
break;
fi
done
if (( !found )); then
echo "Not found"
fi

Not able to skip blank lines in a shell script

I am reading a text file line by line and taking the count of all lines as a part of my requirement.
When there is blank line then it get messed up. I tried with if condition for [ -z "$line" ] , however not able to succeed.
Here is my current code:
countNumberOfCases() {
echo "2. Counting number of test cases -----------"
cd $SCRIPT_EXECUTION_DIR
FILE_NAME=Features
while read line || [[ -n "$line" ]]
do
TEST_CASE="$line"
if [ "${TEST_CASE:0:1}" != "#" ] ; then
cd $MVN_EXECUTION_DIR
runTestCase
fi
done < $FILE_NAME
echo " v_ToalNoOfCases : = " $v_ToalNoOfCases
}
And below is Features file
web/sprintTwo/TC_002_MultipleLoginScenario.feature
#web/sprintOne/TC_001_SendMoneyTransaction_Spec.feature
web/sprintTwo/TC_003_MultipleLoginScenario.feature
#web/sprintOne/TC_004_SendMoneyTransaction_Spec.feature
When there is blank line it wont work properly so my requirement is that if there is blank line then it should be skipped and should not get considered.
You can write your loop in a little more robust way:
#!/bin/bash
while read -r line || [[ $line ]]; do # read lines one by one
cd "$mvn_execution_dir" # make sure this is an absolute path
# or move it outside the loop unless "runTestCase" function changes the current directory
runTestCase "$line" # need to pass the argument?
done < <(sed -E '/^[[:blank:]]*$/d; /^[[:blank:]]+#/d' "$file_name") # strip blanks and comments
A few things:
get your script checked at shellcheck for common mistakes
see this post for proper variable naming convention:
Correct Bash and shell script variable capitalization
see this discussion about [ vs [[ in Bash
Test for non-zero length string in Bash: [ -n “$var” ] or [ “$var” ]
about reading lines from a text file
Looping through the content of a file in Bash

Comparing strings case-insensitively

I want to compare Hello World to hello world. The result should be true, as if they were equal. I'm doing:
while read line; do
newLine="$newLine$line"
done < $1
newp="Hello World"
if (( ${newp,,}==${newLine,,} )); then
echo "true"
else
echo "false"
fi
when I pass a text file consisting of:
#filename: file.txt
hello world
The output seems to be:
./testScript.txt: line 20: 0à»: hello world==hello world : syntax error in expression (error token is "world==hello world ")
+ echo false
What am I doing wrong here? Also, a bit unrelated, is there any way to pass the line that is in file.txt to a string(newLine) without doing that while I have done?
You should add commas and change the double parentheses to single brackets. The if statement should be something like:
if [ "${newp,,}" = "${newLine,,}" ]; then
And in relation to that while loop... It depends on what you want to do. If, like in this case, you want to get the entire file and save it as a single string, you could simply do:
line=$(cat $1)
I would suggest you only use that loop you wrote if you are trying to parse the file line by line, i.e. adding if statements, using different variables and so on. But for a simple case like this one, cat will do just fine.
There is a shell option, nocasematch, that enables case insensitive pattern matching for use with [[ and case.
Comparing strings that differ by casing only:
$ var1=lowercase
$ var2=LOWERCASE
$ [[ $var1 == $var2 ]] && echo "Matches!" || echo "Doesn't match!"
Doesn't match!
Now enabling the shell option and trying again:
$ shopt -s nocasematch
$ [[ $var1 == $var2 ]] && echo "Matches!" || echo "Doesn't match!"
Matches!
Just make sure to turn it off again with shopt -u nocasematch if you don't want to do all comparisons case insensitive.

Bash problems with string comparison

I have a problem with writing bash script. The problem is in comparison of strings. When I launch it, there's no errors. However in result, it is always changing the variable client.
So if for an example we have two lines in file
apple A
orange D
and if I give the who=A I expect to see in result apple, or if at D - orange
But no matter of what I choose A or D it is always giving me the result - orange
No matter of the strings, it always change the variable client, like ignoring the comparison. Please help.
while read line
do
IFS=" "
set -- $line
echo $2" "$who":"$1
if [[ "$2"="$who" ]]
then
echo "change"
client=$1
fi
done < $file
echo $client
So now I changed the code as in one of the comment below, but now the caparison always false therefore the variable client is always empty
while read -r line
do
#IFS=" "
#set -- $line
#echo $2" "$who":"$1
#if [[ "$2" = "$who" ]]
a="${line% *}"
l="${line#* }"
if [[ "$l" == "$who" ]]
then
echo "hi"
client="$a"
fi
done < $file
If you have data in a file with each line like apple D and you want to read the file and separate then items, the parameter expansion/substring extraction is the correct way to process the line. For example (note $who is taken from your problem statement):
while read -r line
do
fruit="${line% *}" # remove from end to space
letter="${line#* }" # remove from start to space
if [[ "$letter" == "$who" ]]
then
echo "change"
client="$fruit"
fi
done < $file
Short Example
Here is a quick example of splitting the words with parameter expansion/substring extraction:
#!/bin/bash
while read -r line
do
fruit="${line% *}"
letter="${line#* }"
echo "fruit: $fruit letter: $letter"
done
exit 0
input
$ cat dat/apple.txt
Apple A
Orange D
output
$ bash apple.sh <dat/apple.txt
fruit: Apple letter: A
fruit: Orange letter: D
Change if [[ "$2"="$who" ]] to
if [[ "$2" = "$who" ]]
spaces around =
Example (for clarification):
who=A
while read line
do
IFS=" "
set -- $line
echo $2" "$who":"$1
if [[ "$2" = "$who" ]]
then
echo "change"
client=$1
fi
done < file #this is the file I used for testing
echo $client
Output:
A A:apple
change
D A:orange
apple
For who=D:
A D:apple
D D:orange
change
orange
You do need spaces around that = operator.
However, I think you're facing yet another issue as you're trying to change the value of the client variable from inside the while loop (which executes in a subshell). I don't think that will work; see this quesion for details.

nested if statement not working using while read variable

I have a csv file that i am reading with a "while read" statement and i want to run an if statement on one of the fields in the csv.
====================================
csv file
client1,admin,password,5.9
client2,admin,password,5.8
====================================
this is my script
while read clientid user pass version
do
if [ '$version' = "5.9" ];
then
echo "IS"
else
echo "NOT"
fi
done < $1
The problem is that the if statement does not work.
It does not echo IS when the version is 5.9, it just keeps saying NOT, unless i change it to !=
I have tried using single and double quotes, even without... still doesn't work as expected.
The goal is to run commands until the end of the file.
Is this script correct for doing this?
Obviously the IS and NOT would be replaced by actual command, this is just for testing.
The sample csv file provided has trailing whitespace on the line, which can be removed from the version variable using parameter expansion.
This should work:
while IFS=, read -r clientid user pass version; do
if [ "${version//[[:space:]]/}" = "5.9" ]; then
echo "IS"
else
echo "NOT"
fi
done < $1
And here's another:
while IFS=$' \t\r\n' read -r line; do
IFS=, read -r clientid user pass version __ <<< "$line"
if [[ $version == '5.9' ]]; then
echo "IS"
else
echo "NOT"
fi
done < "$1"
Quote variables in the open always to prevent word splitting and pathname expansion.
Prefer [[ ]] over [ ]. It doesn't do word splitting and pathname expansion.
IFS=$' \t\r\n' trims out leading and trailing spaces.
__ is added to store surplus values just in case.
You can add the IFS value comma and whitespace IFS=', ' . You will get the exact result.
#!/bin/bash
IFS=', '
while read clientid user pass version
do
if [ "$version" == "5.9" ] ; then
echo "IS"
else
echo "NOT"
fi
done < $1

Resources