Shell script to manipulate if loop - shell

I am trying to modify a if loop in a large code base.My need is as follows.
The code may contain as follows.This is just a random combination example of an if condition.I need to modify if else if conditions alone.
if((A==B)&&(C==D)&&((E==F)||(G==H))||(I)&&(J!=K))
should be modified as
if((string.Compare(A,B)==0)&&(string.Compare(C,D)==0)&&((string.Compare(E,F)==0)||(string.Compare(G,H)==0))||(I)&&(string.Compare(J,K)!=0))
I tried with Java but utterly failed.I believe this is possible with sed or awk.Any help?

You could do it basically with any language that supports regular expressions replacement.
Here's a 3 lines working C# example:
string text = "if((A==B)&&(C==D)&&((E==F)||(G==H))||(I)&&(J!=K))";
string pattern = #"\((?:(\w)((?:=|!)=)(\w))\)";
var replaced = Regex.Replace(text, pattern, m => string.Format("(string.Compare({0},{1}){2}0)", m.Groups[1].Value, m.Groups[3].Value, m.Groups[2].Value));
Console.WriteLine(replaced);
And the result:
if((string.Compare(A,B)==0)&&(string.Compare(C,D)==0)&&((string.Compare(E,F)==0)||(string.Compare(G,H)==0))||(I)&&(string.Compare(J,K)!=0))

sed -r 's/([[:alpha:]])([=!]=)([[:alpha:]])/string.Compare(\1,\3) \2 0/g'
The spaces around \2 aren't strictly necessary.

Basically you don't need to use awk or sed to construct the if statement. It is enough to write:
if [ A == B -a C == D -a ( E == F -o G == H ) -o I -a J != K ]; then
your code here
fi
The == operator compares strings. If you wanted to compare numbers use -eq operator. See this chapter of bash manual (scroll down to test command), and the description of usage of primary conditional expressions

Related

Parse filename string and extract parent at specific level using shell

I have a filename as a string, say filname="a/b/c/d.png".
Is there a general method to extract the parent directory at a given level using ONLY shell parameter expansion?
I.e. I would like to extract "level 1" and return c or "level 2" and return b.
Explicitly, I DO NOT want to get the entire parent path (i.e. a/b/c/, which is the result of ${filename%/*}).
Using just shell parameter expansion, assuming bash, you can first transform the path into an array (splitting on /) and then ask for specific array indexes:
filename=a/b/c/d.png
IFS=/
filename_array=( $filename )
unset IFS
echo "0 = ${filename_array[0]}"
echo "1 = ${filename_array[1]}"
echo "2 = ${filename_array[2]}"
echo "3 = ${filename_array[3]}"
Running the above produces:
0 = a
1 = b
2 = c
3 = d.png
These indexes are the reverse of what you want, but a little
arithmetic should fix that.
Using zsh, the :h modifier trims the final component off a path in variable expansion.
The (s:...:) parameter expansion flag can be used to split the contents of a variable. Combine those with normal array indexing where a negative index goes from the end of the array, and...
$ filename=a/b/c/d.png
$ print $filename:h
a/b/c
$ level=1
$ print ${${(s:/:)filename:h}[-level]}
c
$ level=2
$ print ${${(s:/:)filename:h}[-level]}
b
You could also use array subscript flags instead to avoid the nested expansion:
$ level=1
$ print ${filename[(ws:/:)-level-1]}
c
$ level=2
$ print ${filename[(ws:/:)-level-1]}
b
w makes the index of a scalar split on words instead of by character, and s:...: has the same meaning, to say what to split on. Have to subtract one from the level to skip over the trailing d.png, since it's not stripped off already like the first way.
The :h (head) and :t (tail) expansion modifiers in zsh accept digits to specify a level; they can be combined to get a subset of the path:
> filname="a/b/c/d.png"
> print ${filname:t2}
c/d.png
> print ${filname:t2:h1}
c
> print ${filname:t3:h1}
b
If the level is in a variable, then the F modifier can be used to repeat the h modifier a specific number of times:
> for i in 1 2 3; printf '%s: %s\n' $i ${filname:F(i)h:t}
1: c
2: b
3: a
If using printf (a shell builtin) is allowed then this will do the trick in bash:
filename='a/b/c/d.png'
level=2
printf -v spaces '%*s' $level
pattern=${spaces//?/'/*'}
component=${filename%$pattern}
component=${component##*/}
echo $component
prints out
b
You can assign different values to the variable level.

How to convert a semantic version shell variable to a shifted integer?

Given a shell variable whose value is a semantic version, how can I create another shell variable whose value is (tuple 1 × 1000000) + (tuple 2 × 1000) + (tuple 3) ?
E.g.
$ FOO=1.2.3
$ BAR=#shell magic that, given ${FOO} returns `1002003`
# Shell-native string-manipulation? sed? ...?
I'm unclear about how POSIX-compliance vs. shell-specific syntax comes into play here, but I think a solution not bash-specific is preferred.
Update: To clarify: this isn't as straightforward as replacing "." with zero(es), which was my initial thought.
E.g. The desired output for 1.12.30 is 1012030, not 100120030, which is what a .-replacement approach might provide.
Bonus if the answer can be a one-liner variable-assignment.
A perl one-liner:
echo $FOO | perl -pne 's/\.(\d+)/sprintf "%03d", $1/eg'
How it works:
perl -pne does a REPL with the supplied program
The program contains a replacement function s///
The search string is the regex \.(\d+) which matches a string beginning with dot and ends with digits and capture those digits
The e modifier of the s/// function evaluates the right-hand side of the s/// replacement as an expression. Since we captured the digits, they'll be converted into int and formatted into leading zeros with sprintf
The g modifier replaces all instances of the regex in the input string
Demo
Split on dots, then loop and multiply/add:
version="1.12.30"
# Split on dots instead of spaces from now on
IFS="."
# Loop over each number and accumulate
int=0
for n in $version
do
int=$((int*1000 + n))
done
echo "$version is $int"
Be aware that this treats 1.2 and 0.1.2 the same. If you want to always treat the first number as major/million, consider padding/truncating beforehand.
This should do it
echo $foo | sed 's/\./00/g'
How about this?
$ ver=1.12.30
$ foo=$(bar=($(echo $ver|sed 's/\./ /g')); expr ${bar[0]} \* 1000000 + ${bar[1]} \* 1000 + ${bar[2]})
$ echo $foo
1012030

Bash: How to decode and encode specific URL characters? [duplicate]

is it possible to change multiply patterns to different values at the same command?
lets say I have
A B C D ABC
and I want to change every A to 1 every B to 2 and every C to 3
so the output will be
1 2 3 D 123
since I have 3 patterns to change I would like to avoid substitute them separately.
I thought there would be something like
sed -r s/'(A|B|C)'/(1|2|3)/
but of course this just replace A or B or C to (1|2|3).
I should just mention that my real patterns are more complicated than that...
thank you!
Easy in sed:
sed 's/WORD1/NEW_WORD1/g;s/WORD2/NEW_WORD2/g;s/WORD3/NEW_WORD3/g'
You can separate multiple commands on the same line by a ;
Update
Probably this was too easy. NeronLeVelu pointed out that the above command can lead to unwanted results because the second substitution might even touch results of the first substitution (and so on).
If you care about this you can avoid this side effect with the t command. The t command branches to the end of the script, but only if a substitution did happen:
sed 's/WORD1/NEW_WORD1/g;t;s/WORD2/NEW_WORD2/g;t;s/WORD3/NEW_WORD3/g'
Easy in Perl:
perl -pe '%h = (A => 1, B => 2, C => 3); s/(A|B|C)/$h{$1}/g'
If you use more complex patterns, put the more specific ones before the more general ones in the alternative list. Sorting by length might be enough:
perl -pe 'BEGIN { %h = (A => 1, AA => 2, AAA => 3);
$re = join "|", sort { length $b <=> length $a } keys %h; }
s/($re)/$h{$1}/g'
To add word or line boundaries, just change the pattern to
/\b($re)\b/
# or
/^($re)$/
# resp.
This will work if your "words" don't contain RE metachars (. * ? etc.):
$ cat file
there is the problem when the foo is closed
$ cat tst.awk
BEGIN {
split("the a foo bar",tmp)
for (i=1;i in tmp;i+=2) {
old = (i>1 ? old "|" : "\\<(") tmp[i]
map[tmp[i]] = tmp[i+1]
}
old = old ")\\>"
}
{
head = ""
tail = $0
while ( match(tail,old) ) {
head = head substr(tail,1,RSTART-1) map[substr(tail,RSTART,RLENGTH)]
tail = substr(tail,RSTART+RLENGTH)
}
print head tail
}
$ awk -f tst.awk file
there is a problem when a bar is closed
The above obviously maps "the" to "a" and "foo" to "bar" and uses GNU awk for word boundaries.
If your "words" do contain RE metachars etc. then you need a string-based solution using index() instead of an RE based one using match() (note that sed ONLY supports REs, not strings).
replace with callback function in javascript
similar to the perl solution by choroba
var i = 'abcd'
var r = {ab: "cd", cd: "ab"}
var o = i.replace(/ab|cd/g, (...args) => r[args[0]])
o == 'cdab'
can be optimized with capture groups like /(ab)|(cd)/g
and checking args[i] for undefined values

Replace and increment letters and numbers with awk or sed

I have a string that contains
fastcgi_cache_path /var/run/nginx-cache15 levels=1:2 keys_zone=MYSITEP:100m inactive=60m;
One of the goals of this script is to increment nginx-cache two digits based on the value find on previous file. For doing that I used this code:
# Replace cache_path
PREV=$(ls -t /etc/nginx/sites-available | head -n1) #find the previous cache_path number
CACHE=$(grep fastcgi_cache_path $PREV | awk '{print $2}' |cut -d/ -f4) #take the string to change
SUB=$(echo $CACHE |sed "s/nginx-cache[0-9]*[0-9]/&#/g;:a {s/0#/1/g;s/1#/2/g;s/2#/3/g;s/3#/4/g;s/4#/5/g;s/5#/6/g;s/6#/7/g;s/7#/8/g;s/8#/9/g;s/9#/#0/g;t a};s/#/1/g") #increment number
sed -i "s/nginx-cache[0-9]*/$SUB/g" $SITENAME #replace number
Maybe not so elegant, but it works.
The other goal is to increment last letter of all occurrences of MYSITEx (MYSITEP, in that case, should become MYSITEQ, after MYSITEP, etc. etc and once MYSITEZ will be reached add another letter, like MYSITEAA, MYSITEAB, etc. etc.
I thought something like:
sed -i "s/MYSITEP[A-Z]*/MYSITEGG/g" $SITENAME
but it can't works cause MYSITEGG is a static value and can't be used.
How can I calculate the last letter, increment it to the next one and once the last Z letter will be reached, add another letter?
Thank you!
Perl's autoincrement will work on letters as well as digits, in exactly the manner you describe
We may as well tidy your nginx-cache increment as well while we're at it
I assume SITENAME holds the name of the file to be modified?
It would look like this. I have to assign the capture $1 to an ordinary variable $n to increment it, as $1 is read-only
perl -i -pe 's/nginx-cache\K(\d+)/ ++($n = $1) /e; s/MYSITE\K(\w+)/ ++($n = $1) /e;' $SITENAME
If you wish, this can be done in a single substitution, like this
perl -i -pe 's/(?:nginx-cache|MYSITE)\K(\w+)/ ++($n = $1) /ge' $SITENAME
Note: The solution below is needlessly complicated, because as Borodin's helpful answer demonstrates (and #stevesliva's comment on the question hinted at), Perl directly supports incrementing letters alphabetically in the manner described in the question, by applying the ++ operator to a variable containing a letter (sequence); e.g.:
$ perl -E '$letters = "ZZ"; say ++$letters'
AAA
The solution below may still be of interest as an annotated showcase of how Perl's power can be harnessed from the shell, showing techniques such as:
use of s///e to determine the replacement string with an expression.
splitting a string into a character array (split //, "....")
use of the ord and chr functions to get the codepoint of a char., and convert a(n incremented) codepoint back to a char.
string replication (x operator)
array indexing and slices:
getting an array's last element ($chars[-1])
getting all but the last element of an array (#chars[0..$#chars-1])
A perl solution (in effect a re-implementation of what ++ can do directly):
perl -pe 's/\bMYSITE\K([A-Z]+)/
#chars = split qr(), $1; $chars[-1] eq "Z" ?
"A" x (1 + scalar #chars)
:
join "", #chars[0..$#chars-1], chr (1 + ord $chars[-1])
/e' <<'EOF'
...=MYSITEP:...
...=MYSITEZP:...
...=MYSITEZZ:...
EOF
yields:
...=MYSITEQ:... # P -> Q
...=MYSITEZQ:... # ZP -> ZQ
...=MYSITEAAA:... # ZZ -> AAA
You can use perl's -i option to replace the input file with the result
(perl -i -pe '...' "$SITENAME").
As Borodin's answer demonstrates, it's not hard to solve all tasks in the question using perl alone.
The s function's /e option allows use of a Perl expression for determining the replacement string, which enables sophisticated replacements:
$1 references the current MYSITE suffix in the expression.
#chars = split qr(), $1 splits the suffix into a character array.
$chars[-1] eq "Z" tests if the last suffix char. is Z
If so: The suffix is replaced with all As, with an additional A appended
("A" x (1 + scalar #chars)).
Otherwise: The last suffix char. is replaced with the following letter in the alphabet
(join "", #chars[0..$#chars-1], chr (1 + ord $chars[-1]))

Replace multiple patterns, but not with the same string

is it possible to change multiply patterns to different values at the same command?
lets say I have
A B C D ABC
and I want to change every A to 1 every B to 2 and every C to 3
so the output will be
1 2 3 D 123
since I have 3 patterns to change I would like to avoid substitute them separately.
I thought there would be something like
sed -r s/'(A|B|C)'/(1|2|3)/
but of course this just replace A or B or C to (1|2|3).
I should just mention that my real patterns are more complicated than that...
thank you!
Easy in sed:
sed 's/WORD1/NEW_WORD1/g;s/WORD2/NEW_WORD2/g;s/WORD3/NEW_WORD3/g'
You can separate multiple commands on the same line by a ;
Update
Probably this was too easy. NeronLeVelu pointed out that the above command can lead to unwanted results because the second substitution might even touch results of the first substitution (and so on).
If you care about this you can avoid this side effect with the t command. The t command branches to the end of the script, but only if a substitution did happen:
sed 's/WORD1/NEW_WORD1/g;t;s/WORD2/NEW_WORD2/g;t;s/WORD3/NEW_WORD3/g'
Easy in Perl:
perl -pe '%h = (A => 1, B => 2, C => 3); s/(A|B|C)/$h{$1}/g'
If you use more complex patterns, put the more specific ones before the more general ones in the alternative list. Sorting by length might be enough:
perl -pe 'BEGIN { %h = (A => 1, AA => 2, AAA => 3);
$re = join "|", sort { length $b <=> length $a } keys %h; }
s/($re)/$h{$1}/g'
To add word or line boundaries, just change the pattern to
/\b($re)\b/
# or
/^($re)$/
# resp.
This will work if your "words" don't contain RE metachars (. * ? etc.):
$ cat file
there is the problem when the foo is closed
$ cat tst.awk
BEGIN {
split("the a foo bar",tmp)
for (i=1;i in tmp;i+=2) {
old = (i>1 ? old "|" : "\\<(") tmp[i]
map[tmp[i]] = tmp[i+1]
}
old = old ")\\>"
}
{
head = ""
tail = $0
while ( match(tail,old) ) {
head = head substr(tail,1,RSTART-1) map[substr(tail,RSTART,RLENGTH)]
tail = substr(tail,RSTART+RLENGTH)
}
print head tail
}
$ awk -f tst.awk file
there is a problem when a bar is closed
The above obviously maps "the" to "a" and "foo" to "bar" and uses GNU awk for word boundaries.
If your "words" do contain RE metachars etc. then you need a string-based solution using index() instead of an RE based one using match() (note that sed ONLY supports REs, not strings).
replace with callback function in javascript
similar to the perl solution by choroba
var i = 'abcd'
var r = {ab: "cd", cd: "ab"}
var o = i.replace(/ab|cd/g, (...args) => r[args[0]])
o == 'cdab'
can be optimized with capture groups like /(ab)|(cd)/g
and checking args[i] for undefined values

Resources