How To Split Up Digits Into Character Array - bash

I'm a bit stuck with something. I have a for loop like this:
#!/bin/bash
for i in {10..15}
do
I want to obtain the last digit of the number, so if i is 12, I want to get 2. I'm having difficulties with the syntax though. I've read that I should convert it into a character array, but when I do something like:
j=${i[#]}
echo $j
I don't get 1 0 1 1 1 2 and so on...I get 10, 11, 12...How do I get the numbers to be split up so I can get the last one of i, when I don't always know how many digits will make up i (ex. it may be 1, or 10, or a 100, etc.)?

Trick is to treat $i like a string.
for i in {10..15}; do j="${i: -1}"; echo $j; done
Of course, you do not need to assign to a variable if you don't want to:
for i in {10..15}; do echo "${i: -1}"; done

This answer which uses GNU shell parameter expansion is the most sensible method, I guess.
However, you can also use the double parenthesis construct which allows C-style manipulation of variables in Bash.
for i in {10..15}
do
(( j = i % 10 )) # modulo 10 always gives the ones' digit
echo $j
done

This awk command could solve your problem:
awk '{print substr($0,length,1)}' test_file
I'm assuming that the numbers are saved in a file test_file
If you want to use for loop:
for i in `cat test_1`
do
echo $i |tail -c 2
done

Related

Addin 0 in front of one digit number/text in bash script

I would like to compare a number, which is the output of a command, with a constant and do some manipulation. That is, if $id < 10, I want to see 590$id and if it is above 10, I want to see 59$id.
I found that expr doesn't working here:
ID=3
NUM=59$ID
if [ `expr $ID` -lt 10]; then
NUM=590$ID
fi
echo $NUM
The output of the code is 593 and not 5903. Even, $(($ID + 5900)) -lt 5910 writes 593.
How can I fix that?
Could you please try following.
cat script.sh
#!/bin/bash
ID=$(printf "%02d" 3 )
##NUM=59$ID ##Commented this to check if, condition is getting satisfied or not. Doesn't seem to be fit here.
(( $ID < 10 )) && NUM="59$ID"
echo "$NUM"
Output will be 5903 after running above code.
Don't use expr. It's old and tricky.
Don't use backticks `. They are discouraged and $( ... ) is preferred.
For arithmetic comparisons use arithmetic expansions. Just
if (( ID < 10 )); then
Note that bash is space aware and your script has a syntax error, it is missing a space - the 10]; should be 10 ];.
Note that by convention uppercase variables should be used for exported variables.
Looking at your code I think you just want:
NUM=$((5900 + ID))

why grep does not return all pari of numbers?

Executing echo "123456" | grep -Eo "[[:digit:]]{1,2}" will return tree pairs, 12, 34, 56.
Why it does not return 12, 23, 34, 45, 56?
Your regular expression is not printing all of 5 pairs of numbers because you are asking only for three.
Your regex is the equivalent of [0-9][0-9] and will check that particular match starting from left; so if you have 123456 the steps would be something like:
1 -> Match? No; don't print anything.
12 -> Match? Yes; print it.
3 -> Match? No; don't print anything.
And so on...
Note that it doesn't start again after a match, otherwise it would match 12 over and over again...
You can use other solutions for your problem.
For example, if you need all pairs in that string you can use a function that take the first two numbers; cut the first one and check again, until the string is too short...
#!/bin/bash
check_pairs() {
local str="${1}"
if [ "${#str}" -ge 2 ]; then
printf "%s\n" "${str}" | sed -e "s/^\([0-9][0-9]\).*$/\1/"
check_pairs "${str#?}"
fi
}
check_pairs "123456"
exit 0
Probably there are other solutions (better, faster, stronger), but I cannot think of them right now.
While you already have a valid answer, if your shell is bash (or another advanced shell that allows string indexes), it would be far more efficient to use bash's built-in string indexing and a C-style for loop to output each pair in a given string rather than spawning a separate subshell on each loop iteration created by calling sed.
Bash string indexing allows you to access len characters within a string beginning at start index (where the valid indexes are 0 - len-1) using the form:
${var:$start:$len}
Combined with a C-style for loop looping over each index i in the string beginning at index 1 (the 2nd character) and outputting the pair of characters created by:
"${var:$((i-1)):2}"
A short example would be:
str=123456
for ((i = 1; i < ${#str}; i++)); do
echo "${str:$((i-1)):2}"
done
Example Use/Output
$ str=123456; \
for ((i = 1; i < ${#str}; i++)); do echo "${str:$((i-1)):2}"; done
12
23
34
45
56
Look things over and let me know if you have further questions.

Is there a way to implement a counter in bash but for letters instead of numbers?

I'm working with an existing script which was written a bit messily. Setting up a loop with all of the spaghetti code could make a bigger headache than I want to deal with in the near term. Maybe when I have more time I can clean it up but for now, I'm just looking for a simple fix.
The script deals with virtual disks on a xen server. It reads multipath output and asks if particular LUNs should be formatted in any way based on specific criteria. However, rather than taking that disk path and inserting it, already formatted, into a configuration file, it simply presents every line in the format
'phy:/dev/mapper/UUID,xvd?,w',
UUID, of course, is an actual UUID.
The script actually presents each of the found LUNs in this format expecting the user to copy and paste them into the config file replacing each ? with a letter in sequence. This is tedious at best.
There are several ways to increment a number in bash. Among others:
var=$((var+1))
((var+=1))
((var++))
Is there a way to do the same with characters which doesn't involve looping over the entire alphabet such that I could easily "increment" the disk assignment from xvda to xvdb, etc?
To do an "increment" on a letter, define the function:
incr() { LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'$1")+1))")"; }
Now, observe:
$ echo $(incr a)
b
$ echo $(incr b)
c
$ echo $(incr c)
d
Because, this increments up through ASCII, incr z becomes {.
How it works
The first step is to convert a letter to its ASCII numeric value. For example, a is 97:
$ printf '%d' "'a"
97
The next step is to increment that:
$ echo "$((97+1))"
98
Or:
$ echo "$(($(printf '%d' "'a")+1))"
98
The last step is convert the new incremented number back to a letter:
$ LC_CTYPE=C printf "\\$(printf '%03o' "98")"
b
Or:
$ LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'a")+1))")"
b
Alternative
With bash, we can define an associative array to hold the next character:
$ declare -A Incr; last=a; for next in {b..z}; do Incr[$last]=$next; last=$next; done; Incr[z]=a
Or, if you prefer code spread out over multiple lines:
declare -A Incr
last=a
for next in {b..z}
do
Incr[$last]=$next
last=$next
done
Incr[z]=a
With this array, characters can be incremented via:
$ echo "${Incr[a]}"
b
$ echo "${Incr[b]}"
c
$ echo "${Incr[c]}"
d
In this version, the increment of z loops back to a:
$ echo "${Incr[z]}"
a
How about an array with entries A-Z assigned to indexes 1-26?
IFS=':' read -r -a alpharray <<< ":A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z"
This has 1=A, 2=B, etc. If you want 0=A, 1=B, and so on, remove the first colon.
IFS=':' read -r -a alpharray <<< "A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z"
Then later, where you actually need the letter;
var=$((var+1))
'phy:/dev/mapper/UUID,xvd${alpharray[$var]},w',
The only problem is that if you end up running past 26 letters, you'll start getting blanks returned from the array.
Use a Bash 4 Range
You can use a Bash 4 feature that lets you specify a range within a sequence expression. For example:
for letter in {a..z}; do
echo "phy:/dev/mapper/UUID,xvd${letter},w"
done
See also Ranges in the Bash Wiki.
Here's a function that will return the next letter in the range a-z. An input of 'z' returns 'a'.
nextl(){
((num=(36#$(printf '%c' $1)-9) % 26+97));
printf '%b\n' '\x'$(printf "%x" $num);
}
It treats the first letter of the input as a base 36 integer, subtracts 9, and returns the character whose ordinal number is 'a' plus that value mod 26.
Use Jot
While the Bash range option uses built-ins, you can also use a utility like the BSD jot utility. This is available on macOS by default, but your mileage may vary on Linux systems. For example, you'll need to install athena-jot on Debian.
More Loops
One trick here is to pre-populate a Bash array and then use an index variable to grab your desired output from the array. For example:
letters=( "" $(jot -w %c 26 a) )
for idx in 1 26; do
echo ${letters[$idx]}
done
A Loop-Free Alternative
Note that you don't have to increment the counter in a loop. You can do it other ways, too. Consider the following, which will increment any letter passed to the function without having to prepopulate an array:
increment_var () {
local new_var=$(jot -nw %c 2 "$1" | tail -1)
if [[ "$new_var" == "{" ]]; then
echo "Error: You can't increment past 'z'" >&2
exit 1
fi
echo -n "$new_var"
}
var="c"
var=$(increment_var "$var")
echo "$var"
This is probably closer to what the OP wants, but it certainly seems more complex and less elegant than the original loop recommended elsewhere. However, your mileage may vary, and it's good to have options!

Bash read multiple files in for loop (as a execute arguement)

I am trying to read a file in bash forloop. But I do not know how to put write the script for this.
for i in $( seq 0 $step 10 )
do
echo "Rendering: "$(( i + j ))
python auto_fine.py density000000.vtu velocity000000.vtu $(( i + j ))
done
each and every loop I need to call
i -> 0 python auto_fine.py density000000.vtu velocity000000.vtu
i -> 1 python auto_fine.py density000010.vtu velocity000010.vtu
i -> 2 python auto_fine.py density000020.vtu velocity000020.vtu
It seems to me that you need to zero pad the numbers sed provides to you:
As seen in How to zero pad a sequence of integers in bash so that all have the same width?, you need to do something like
$ seq -f "%06g" 0 10 100
Which returns:
000000
000010
000020
...
000100
All together,
for i in $(seq -f "%06g" 0 10 100)
do
# echo "Rendering: "$(( i + j )) -- not sure what this does
python auto_fine.py density$i.vtu velocity$i.vtu
done
Bash can do this without requiring external tools like seq.
for i in {0..100}; do
[[ $i = *0 ]] || continue
python auto_fine.py density$(printf '%06d' $i).vtu velocity$(printf '%06d' $i).vtu
done
This uses pattern matching (*0) to limit your list to every 10 numbers, which is a bit of a hack, but will work against your sample data.
You could alternately loop against your zero-padded numeric strings directly:
for i in $(printf '%05d0 ' {0..10}); do
python auto_fine.py density$i.vtu velocity$i.vtu
done
This option shows you every 10 items by placing a zero in the printf format after the incrementing number, which becomes the tens digit. If you want more arbitrary sequencing, you might use multipliers, still without spawning external processes:
low=0
high=100
mult=10
for i in $(eval echo {$low..$((high/mult))}); do
n=$(printf '%06d' $((i*mult)))
python auto_fine.py density$n.vtu velocity$n.vtu
done
Note the eval, which lets you expand variables for use in your sequence expression. (If you are getting these numbers from an external source, have your script validate them before using them!)
If you're using bash version 4 (i.e. not the native version on OSX), you also have increments available in sequence expressions. From the man page:
A sequence expression takes the form {x..y[..incr]}, where x and y are
either integers or single characters, and incr, an optional increment,
is an integer.
So perhaps:
low=0
high=100
mult=10
for i in $(eval "printf '%06d ' {$low..$high..$mult}"); do
python auto_fine.py density$i.vtu velocity$i.vtu
done
Note that in sequence expressions, the first member of the sequence is the first number provided, rather than merely a product of a multiplier. We have quotes around the printf to ensure that the sequence expression is expanded by eval, and not interpreted by the command substitution ($(..)).
looping for all the files in the current dir is trivial:
for i in $( ls -1 )
do
# your code here, variable is referenced with $i
done
what's the j variable you are using?

Convert string into integer in bash script - "Leading Zero" number error

In a text file, test.txt, I have the next information:
sl-gs5 desconnected Wed Oct 10 08:00:01 EDT 2012 1001
I want to extract the hour of the event by the next command line:
hour=$(grep -n sl-gs5 test.txt | tail -1 | cut -d' ' -f6 | awk -F ":" '{print $1}')
and I got "08". When I try to add 1,
14 echo $((hour+1))
I receive the next error message:
./test2.sh: line 14: 08: value too great for base (error token is "08")
If variables in Bash are untyped, why?
See ARITHMETIC EVALUATION in man bash:
Constants with a leading 0 are interpreted as octal numbers.
You can remove the leading zero by parameter expansion:
hour=${hour#0}
or force base-10 interpretation:
$((10#$hour + 1))
what I'd call a hack, but given that you're only processing hour values, you can do
hour=08
echo $(( ${hour#0} +1 ))
9
hour=10
echo $(( ${hour#0} +1))
11
with little risk.
IHTH.
You could also use bc
hour=8
result=$(echo "$hour + 1" | bc)
echo $result
9
Here's an easy way, albeit not the prettiest way to get an int value for a string.
hour=`expr $hour + 0`
Example
bash-3.2$ hour="08"
bash-3.2$ hour=`expr $hour + 0`
bash-3.2$ echo $hour
8
In Short: In order to deal with "Leading Zero" numbers (any 0 digit that comes before the first non-zero) in bash
- Use bc An arbitrary precision calculator language
Example:
a="000001"
b=$(echo $a | bc)
echo $b
Output: 1
From Bash manual:
"bc is a language that supports arbitrary precision numbers with interactive execution
of statements. There are some similarities in the syntax to the C programming lan-
guage. A standard math library is available by command line option. If requested, the
math library is defined before processing any files. bc starts by processing code from
all the files listed on the command line in the order listed. After all files have
been processed, bc reads from the standard input. All code is executed as it is read.
(If a file contains a command to halt the processor, bc will never read from the standard input.)"
Since hours are always positive, and always 2 digits, you can set a 1 in front of it and subtract 100:
echo $((1$hour+1-100))
which is equivalent to
echo $((1$hour-99))
Be sure to comment such gymnastics. :)
The leading 0 is leading to bash trying to interpret your number as an octal number, but octal numbers are 0-7, and 8 is thus an invalid token.
If I were you, I would add some logic to remove a leading 0, add one, and re-add the leading 0 if the result is < 10.
How about sed?
hour=`echo $hour|sed -e "s/^0*//g"`

Resources