Why IFS impact subshell variable? - ksh

This KSH code working fine on AIX and RH with the standard IFS (default)
[ "$(uname)" = "AIX" ] && ECHO="echo" || ECHO="echo -e"
echo "AA BB CC DD" | while read a b c d; do
[ "`$ECHO $a | cut -c1-2`" = "AA" ] && echo $b
done
If i set a different IFS like a ; for example
[ "$(uname)" = "AIX" ] && ECHO="echo" || ECHO="echo -e"
IFS=";"
echo "AA;BB;CC;DD" | while read a b c d; do
[ "`$ECHO $a | cut -c1-2`" = "AA" ] && echo $b
done
I get an error like only on RH not Aix
+ echo 'AA;BB;CC;DD'
+ read a b c d
+ cut -c1-2
+ 'echo -e' AA
-ksh: line 3: echo -e: not found
+ [ '' '=' AA ]
+ read a b c d
+ echo .....
+ cut -d. -f1
Someone can explain me why this is happening and how we can solved it without a eval ?
Thanks for your time :-)

You have ECHO="echo -e", then you set IFS=";", so when you expand $ECHO unquoted (where globbing and word splitting will take effect), you get echo -e as a single word (because you are word splitting using semicolon not whitespace), and of course there's no such command.
You'll want to refer to I'm trying to put a command in a variable, but the complex cases always fail. The answer is to use an array:
[ "$(uname)" = "AIX" ] && ECHO=(echo) || ECHO=(echo -e)
And then, use it like
"${ECHO[#]}" "$a" | cut -c1-2
You'll also need to use quotes more. Refer to Security implications of forgetting to quote a variable in bash/POSIX shells
(todo: discuss not using "echo -e"...)

Related

How to insert variable and code after pattern using sed?

I'm using a shell script to insert code with a variable after a previous code pattern in script.tex, however sed is not adding anything after the expected pattern.
cat script.tex
\multicolumn{1}{c}{st_var}
Expected result (script.tex) after script.sh is run:
\multicolumn{1}{c}{A} & \multicolumn{1}{c}{B} & \multicolumn{1}{c}{C} & \multicolumn{1}{c}{D} & \multicolumn{1}{c}{E} & \multicolumn{1}{c}{F} \\
Current result (script.tex):
\multicolumn{1}{c}{A}
The first part of the conditional is working as expected. The remaining is not being found by sed.
cat script.sh:
#!/bin/bash
var=("NA" "A" "B" "C" "D" "E" "F")
clen=$(( ${#var[#]} - 1 ))
cind=1
for (( i=1; i<${#var[#]}; i++ )) ; do
if [[ "$cind" -eq 1 ]]; then
sed -i 's/st_var/'${var[$i]//\"/}'/g' script.tex
elif [[ "$cind" -gt 1 ]] && [[ "$cind" -lt "$clen" ]]; then
sstr="\multicolumn{1}{c}{${var[$i-1]//\"/}}"
estr=" & \multicolumn{1}{c}{${var[$i]//\"/}}"
festr=" & \multicolumn{1}{c}{${var[$i]//\"/}} \\\\"
sed -i '/^${sstr}/ s/$/${estr}/' script.tex
else
sed -i '/^${sstr}/ s/$/${festr}/' script.tex
fi
cind=$((cind + 1))
done
The var array here must have all elements double quoted for other purposes outside of this question. Also, the var array is shown here for simplicity - the letters A-F could be any random string. The first element in the array here is skipped (NA).
The best attempt so far:
script.sh:
#!/bin/bash -x
var=("NA" "A" "B" "C" "D" "E" "F")
clen=$(( ${#var[#]} - 1 ))
cind=1
for (( i=1; i<${#var[#]}; i++ )) ; do
if [[ "$cind" -eq 1 ]]; then
sed -i 's/st_var/'${var[$i]//\"/}'/g' script.tex
elif [[ "$cind" -gt 1 ]] && [[ "$cind" -lt "$clen" ]]; then
sstr='\multicolumn{1}{c}{'${var[$i-1]//\"/}'}'
estr=' \& \multicolumn{1}{c}{'${var[$i]//\"/}'}'
festr=' \& \multicolumn{1}{c}{'${var[$i+1]//\"/}'} \\'
# sed -i '/$sstr/r $estr/' script.tex
# sed -i '/^'"${sstr}"'/'"${estr}"'/' script.tex
sed -i "s/$sstr/&$estr/" script.tex
else
sed -i "s/$sstr/&$festr/" script.tex
# sed -i '/^'"${sstr}"'/'"${festr}"'/' script.tex
fi
cind=$((cind + 1))
done
Result:
\multicolumn{1}{c}{A} & multicolumn{1}{c}{B} & multicolumn{1}{c}{C} & multicolumn{1}{c}{D} & multicolumn{1}{c}{F} \ & multicolumn{1}{c}{E}
The ampersands are coming through, however the backslashes before multicolumn aren't coming through, and neither are the two backslashes at the end of the line. E and F are also flipped - F should be last.
Consider a different approach. Instead of adding anything incrementally, which might be hard and confusing because you have to keep "state", just do one single run. One replacement and regex pattern.
var=("A" "B" "C" "D" "E" "F")
# Generate replacement for the line.
repl=$(
# Print var on separate lines with the stub
printf " \multicolumn{1}{c}{%s} \n" "${var[#]}" |
# join lines with & + space character
paste -sd '&'
)
# add trailing \\
repl+="\\\\"
# Remove leading space
repl=${repl:1}
# Properly escape
# see https://stackoverflow.com/questions/407523/escape-a-string-for-a-sed-replace-pattern
ESCAPED_REPLACE=$(printf '%s\n' "$repl" | sed -e 's/[\/&]/\\&/g')
KEYWORD="\multicolumn{1}{c}{st_var}";
ESCAPED_KEYWORD=$(printf '%s\n' "$KEYWORD" | sed -e 's/[]\/$*.^[]/\\&/g');
# Finally run sed
set -x
sed "s/^$ESCAPED_KEYWORD$/$ESCAPED_REPLACE/"
When executed, for the following input:
\multicolumn{1}{c}{st_var}
outputs:
+ sed 's/^\\multicolumn{1}{c}{st_var}$/\\multicolumn{1}{c}{A} \& \\multicolumn{1}{c}{B} \& \\multicolumn{1}{c}{C} \& \\multicolumn{1}{c}{D} \& \\multicolumn{1}{c}{E} \& \\multicolumn{1}{c}{F} \\\\/'
\multicolumn{1}{c}{A} & \multicolumn{1}{c}{B} & \multicolumn{1}{c}{C} & \multicolumn{1}{c}{D} & \multicolumn{1}{c}{E} & \multicolumn{1}{c}{F} \\
The following code works:
#!/bin/bash -x
var=("NA" "A" "B" "C" "D" "E" "F")
clen=$(( ${#var[#]} - 1 ))
cind=1
for (( i=1; i<${#var[#]}; i++ )) ; do
if [[ "$cind" -eq 1 ]]; then
sed -i 's/st_var/'${var[$i]//\"/}'/g' script.tex
elif [[ "$cind" -gt 1 ]] && [[ "$cind" -lt "$clen" ]]; then
sstr='\\multicolumn{1}{c}{'${var[$i-1]//\"/}'}'
estr=' \& \\multicolumn{1}{c}{'${var[$i]//\"/}'}'
festr=' \& \\multicolumn{1}{c}{'${var[$i+1]//\"/}'} \\\\'
sed -i "s/$sstr/&$estr/" script.tex
else
sed -i "s/$estr/&$festr/" script.tex
fi
cind=$((cind + 1))
done
This might work for you (GNU sed):
sed -E 's/\\multicolumn\{1\}\{c\}\{st_var\}/ ABCDEF\n&/
:a;ta;s/(\S)(\S*\n(.*)\{st_var\})/\3{\1} \& \2/;ta
s/ (.*)\&.*/\1\\\\/' file
Prepend a space, the values to substituted for st_var and a newline to the original sting \multicolumn{1}{c}{st_var}.
Iterate through each value prepending the original string with the new value substituted until no more values to be substituted exist.
Clean up the new string, removing the introduced newline and the original string and append \\.

What is wrong in this if condition?

This is my code
ports="161,123"
portsArr=$(echo "${ports}" | tr "," "\n")
for port in "${portsArr[#]}"
do
echo "${port}"
if [ "${port}" = "161" ]; then
echo "161";
fi
if [ "${port}" = "123" ]; then
echo "123";
fi
done
For some reason, the if conditions in this code is not working. Although, I'm getting expected results in the Line 5 echo command. Can somebody please explain what is wrong here?
To declare an array, you need some ( ):
portsArr=( $(echo "${ports}" | tr "," "\n") )
You should consider using bash's test : [[ ]]
[[
is a bash keyword similar to (but more powerful than) the [ command. See
http://mywiki.wooledge.org/BashFAQ/031 and
http://mywiki.wooledge.org/BashGuide/TestsAndConditionals
Unless you're writing for POSIX sh, we recommend [[.

How to test a grep -A 1 variable with a condition (syntaxe issue) - Shell Script

Hello i am very new in the programmation and i try to learn by myself shell script.
I have to put a condition on a number after a word (Tetrahedra) i know in a file test.txt ; i have written this :
var=`grep -A 1 Tetrahedra test.txt`
if [ "$var" = "Tetrahedra 0" ]
then
#some action
fi
But i have an issue syntaxe since echo $var prompt
EDIT:
Tetrahedra
0
I don t know how to put the right syntaxe on my test (i try Tetrahedra\n0) i think i miss the line break
Or maybe there is an easy way to do what i want ? I have thought about a solution but it s very messy (like editing a new file, testing it and then deleting it)
Thanks
You need to create a newline using the bash $'\n' syntax. Octal Dump od -c is useful for seeing exactly what characters you have, since it can show non-printing characters using escapes.
test.txt
Tetrahedra
0
Polygon
1
tetra.sh
#!/bin/bash
var=`/bin/grep -A 1 Tetrahedra test.txt`
echo -n "$var" | od -c
match="Tetrahedra"$'\n'"0"
echo -n "$match" | od -c
if [ "$var" == "$match" ]; then
echo "YES"
else
echo "NO"
fi
Output
bash tetra.sh
0000000 T e t r a h e d r a \n 0
0000014
0000000 T e t r a h e d r a \n 0
0000014
YES

Bash variable substitution and strings

Let's say I have two variables:
a="AAA"
b="BBB"
I read a string from a file. This string is the following:
str='$a $b'
How to create a new string from the first one that substitutes the variables?
newstr="AAA BBB"
bash variable indirection whithout eval:
Well, as eval is evil, we may try to make this whithout them, by using indirection in variable names.
a="AAA"
b="BBB"
str='$a $b'
newstr=()
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=($cnt)
done
newstr="${newstr[*]}"
echo $newstr
AAA BBB
Another try:
var1="Hello"
var2="2015"
str='$var1 world! Happy new year $var2'
newstr=()
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=($cnt)
done
newstr="${newstr[*]}"
echo $newstr
Hello world! Happy new year 2015
Addendum As correctly pointed by #EtanReisner's comment, if your string do contain some * or other glob expendable stings, you may have to use set -f to prevent bad things:
cd /bin
var1="Hello"
var2="star"
var3="*"
str='$var1 this string contain a $var2 as $var3 *'
newstr=()
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt};
newstr+=("$cnt");
done;
newstr="${newstr[*]}"
echo "$newstr"
Hello this string contain a star as * bash bunzip2 busybox....zmore znew
echo ${#newstr}
1239
Note: I've added " at newstr+=("$cnt"); to prevent glob expansion, but set -f seem required...
newstr=()
set -f
for cnt in $str ;do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=("$cnt")
done
set +f
newstr="${newstr[*]}"
echo "$newstr"
Hello this string contain a star as * *
Nota 2: This is far away from a perfect solution. For sample if string do contain ponctuation, this won't work again... Example:
str='$var1, this string contain a $var2 as $var3: *'
with same variables as previous run will render:
' this string contain a star as *' because ${!var1,} and ${!var3:} don't exist.
... and if $str do contain special chars:
As #godblessfq asked:
If str contains a line break, how do I do the substitution and preserve the newline in the output?
So this is not robust as every indirected variable must be first, last or space separated from all special chars!
str=$'$var1 world!\n... 2nd line...'
var1=Hello
newstr=()
set -f
IFS=' ' read -d$'\377' -ra array <<<"$str"
for cnt in "${array[#]}";do
[ "${cnt:0:1}" == '$' ] && cnt=${cnt:1} && cnt=${!cnt}
newstr+=("$cnt")
done
set +f
newstr="${newstr[*]}"
echo "$newstr"
Hello world!
... 2nd line...
As <<< inline string add a trailing newline, last echo command could be written:
echo "${newstr%$'\n'}"
The easiest solution is to use eval:
eval echo "$str"
To assign it to a variable, use command substitution:
replaced=$(eval echo "$str")
Disclaimer: I only discovered perl an hour ago. But this seems to work robustly, whatever special characters you throw at it:
newstr=$(a2="$a" b2="$b" perl -pe 's/\$a\b/$ENV{a2}/g; s/\$b\b/$ENV{b2}/g' <(echo -e "$str"))
Test:
a='A*A\nA'
b='B*B\nB'
str='$a $aa * \n $b $bb'
newstr=$(a2="$a" b2="$b" perl -pe 's/\$a\b/$ENV{a2}/g; s/\$b\b/$ENV{b2}/g' <(echo -e "$str"))
echo -e "$newstr"
Output:
A*A
A $aa *
B*B
B $bb
I'd use awk solution with awk-variables. This will allow passing a text containing special chars and subsitute any placeholder with it.
a workaround to recognize $ would be using [\x24]:
awk -v a="$a" -v b="$b" '{gsub("[\x24]a",a);gsub("[\x24]b",b); print}' <<< $str
here
-v defines variable a="$a"
[x24] is ASCII for $, so [x24]a equal to $a
gsub(x,y) - replaces x with y

Assignment (str=$*) trying to run as a command in bash

input in terminal:
cd Desktop
chmod 777 isPalindromeFun.sh
./isPalindromeFun.sh
isPalindrome madam
gives an error: str=madam not found
#!/bin/bash
function isPalindrome () {
if [ "$#" -ne 0 ];
then
inc=$1; # if it has arguments set the increment with the first argument
fi
[ $# -eq 0 ] && { read str ;} || \ str=$*
String="$(echo $str | sed 's/[^[:alnum:]]//g' | \
tr '[:upper:]' '[:lower:]')"
if [ "$(echo $String | rev)" = "$String" ]
then
echo "\"$str\" is a palindrome"
else
echo "\"$str\" is not a palindrome"
fi
}
The backslash needs to be removed when writing this as a single line:
[ "$#" -eq 0 ] && { read str ;} || \ str=$*
It would make more sense if writing the code as two lines:
[ "$#" -eq 0 ] && { read str ;} || \
str=$*
On just one line, however, the backslash only has the effect of escaping the character following it -- making that next character, a space, be data rather than syntax -- and thus makes your command
str=$*
instead be the same as
" str="$*
meaning the space is part of the command. That's not a valid assignment, and of course there's no command called " str=madam" (starting with a space!) on your system.
Aside: $# doesn't strictly need to be quoted -- unless you have IFS set to a value that contains numbers, the results from expanding $# are guaranteed to expand to a single word -- but I'm doing so here as a workaround for StackOverflow's syntax highlighting.

Resources