Shift between two characters - bash

How to get a shift between two characters in bash?
For instance, in C++ we have:
'c'-'a'=2
Are there any elegant solutions?

Define ord to get the ASCII value of each character (from Unix & Linux Stack Exchange, Bash FAQ):
ord() { LC_CTYPE=C printf '%d' "'$1"; }
(note that the ' is not a typo! It is required for printf to treat a character as a number1)
Then you can subtract one from the other:
$ echo "$(( "$(ord c)" - "$(ord a)" ))"
2
If you wanted to put this in a function, you could:
diff_ord() { echo "$(( "$(ord $1)" - "$(ord $2)" ))"; }
Then call it like:
$ diff_ord c a
2
If the leading character is a single-quote or double-quote, the value shall be the numeric value in the underlying codeset of the character following the single-quote or double-quote.

Related

Format currency in Bash

Is it possible to format currency in Bash?
Example data is received as
19366
Data to be displayed as
$193,66
Thanks.
Simply handle your value as a text string, instead of a number, and insert a dollar sign and a comma at the correct positions:
$ v=19366
$ printf '$%s,%s\n' "${v:0: -2}" "${v: -2}"
$193,66
${v:offset:length) expands as the substring of $v that starts at character offset (counting from 0) and which length is length. But negative offsets and lengths can be used to refer to the end of the string.
${v:0:-2} expands as the substring of $v that starts at the beginning (0) and which length is the number of remaining characters minus two (-2). In our example this is 193.
${v: -2} expands as the substring of $v that starts two characters before the end (-2) and which length (not specified) is the number of remaining characters. In our example this is 66. Note the space between : and -2, it is needed to avoid another interpretation by the shell (providing default value 2 if v is unset or null).
Preamble In your request, you use coma , as decimal separator (radix mark). For locale support, see second part of my answer.
1. Pseudo floating poing using integer as strings
I often use this kind of pseudo float:
amount=123456
amount=00$amount # avoid bad length error
printf '$%.2f\n' ${amount::-2}.${amount: -2}
$1234.56
for amount in 0 1 12 123 1234 12345;do
amount=00$amount
printf '$%.2f\n' ${amount::-2}.${amount: -2}
done
$0.00
$0.01
$0.12
$1.23
$12.34
$123.45
As a function:
int2amount() {
if [[ $1 == -v ]]; then
local -n _out="$2"
shift 2
else
local _out
fi
local _amount=00$(($1))
printf -v _out $'$%\47.2f' ${_amount::-2}.${_amount: -2}
[[ ${_out#A} != _out=* ]] || echo "$_out"
}
Then
int2amount 123456
$1’234.56
int2amount -v var 1234567
echo $var
$12’345.67
2. Remark regarding locale, decimal separator and thousand separators
In your request, your radix mark is a coma ,. This depend on your locale configuration. U could hit something like:
set | grep ^LC\\\|^LANG
to show how this is configured on your host.
As there are many issues regarding locales, I've asked How to determine which character is used as decimal separator or thousand separator under current locale as separated question.
Try:
for locvar in C en_US.UTF-8 de_DE.UTF-8 ;do
LC_NUMERIC=$locvar int2amount 1234567
done
$12345.67
$12,345.67
bash: line 1: printf: 0012345.67: invalid octal number
$12.345,00
Error because unsing de_DE locale configuration, you have to use a coma as separator (Decimal separator at wikipedia).
This is already know to produce issues using bc: How do I change the decimal separator in the printf command in bash?
Final function unsing variable decimal separator
int2amount () {
if [[ $1 == -v ]]; then
local -n _out="$2"
shift 2
else
local _out
fi
local _amount=00$(($1)) _decsep
printf -v _decsep %.1f 1
_decsep=${_decsep:1:1}
printf -v _out '$%'\''.2f' ${_amount::-2}${_decsep}${_amount: -2}
[[ ${_out#A} != _out=* ]] || echo "$_out"
}
for locvar in C en_US.UTF-8 de_DE.UTF-8 ;do
LC_NUMERIC=$locvar int2amount 1234567
done
$12345.67
$12,345.67
$12.345,67
Note about LC_ALL: If in your environment, a variable $LC_ALL is defined, all demos using LC_NUMERIC won't work because LC_ALL is over. You have to unset LC_ALL or use:
LC_ALL=$locvar LC_NUMERIC=$locvar int2amount 1234567
in last demo.
You can use printf
amount="240570.578"
printf "%'.2f\n" $amount
> 240,570.58
printf does have a thousands grouping format specifier flag, however the character used to denote the groups (non-monetary grouping character) depends on locale (LC_NUMERIC).
The C or POSIX locale uses no grouping character. Therefore you can't do this portably with printf.
printf "%'d\n" 19366
Works if the current locale supports the comma grouping character.
In my bashrc, I use the following function to add thousands groupings to any integer, using comma (,) and preserving a non numeric prefix (like $, or - for negative numbers). It doesn't depend on locale, but does require rev.
commafy ()
{
printf %s "${1%%[0-9]*}"
printf '%s\n' "${1##*[!0-9]}" |
rev |
sed -E 's/[0-9]{3}/&,/g; s/,$//' |
rev
}
Example:
commafy '$19366'
# gives
$19,366
You could slightly simplify this too:
printf %s \$
printf '%s\n' 19366 |
rev |
sed -E 's/[0-9]{3}/&,/g; s/,$//' |
rev
Simplistically -
$: sed -E 's/([0-9]*)([0-9][0-9])$/$\1,\2/'<<<"19366"
$193,66

Bash math expression

I need help with this its been busting my mind.
I have a read with a variable with integers 10 20 -30.
All separated by white space. I try to change the minus to plus and save it onto another variable but it's not saving. If I can't change to plus I would like to remove it so then I can do:
var=$((${num// /+/}))
So it can add all integers.
This is what I have:
read num
echo $num
sum=$num | sed -e 's/-/+/g'
echo $sum
Using standard POSIX variable expansion and arithmetic:
#!/usr/bin/env sh
# Computes the sum of all arguments
sum () {
# Save the IFS value
_OIFS=$IFS
# Set the IFS to + sign
IFS=+
# Expand the arguments with the IFS + sign
# inside an arithmetic expression to get
# the sum of all arguments.
echo "$(($*))"
# Restore the original IFS
IFS=$_OIFS
}
num='10 20 -30'
# shellcheck disable=SC2086 # Intended word splitting of string into arguments
sum $num
More featured version with a join function:
#!/usr/bin/env sh
# Join arguments with the provided delimiter character into a string
# $1: The delimiter character
# $#: The arguments to join
join () {
# Save the IFS value
_OIFS=$IFS
# Set the IFS to the delimiter
IFS=$1
# Shift out the delimiter from the arguments
shift
# Output the joined string
echo "$*"
# Restore the original IFS
IFS=$_OIFS
}
# Computes the sum of all arguments
sum () {
# Join the arguments with a + sign to form a sum expression
sum_expr=$(join + "$#")
# Submit the sum expression to a shell's arithmetic expression
# shellcheck disable=SC2004 # $sum_expr needs its $ to correctly split terms
echo "$(($sum_expr))"
}
num='10 20 -30'
# shellcheck disable=SC2086 # Intended word splitting of string into arguments
sum $num
Simply: whipe last slash:
num="10 20 -30"
echo $((${num// /+}))
0
Some details
*Bash battern substitution has nothing common with so called regular expression. Correct syntax is:
${parameter/pattern/string}
... If pattern begins with /, all matches of pattern are replaced with string.
Normally only the first match is replaced. ...
See: man -Pless\ +/parameter.pattern.string bash
If you try your syntax:
echo ${num// /+/}
10+/20+/-30
Then
echo ${num// /+}
10+20+-30
Or even, to make this pretty:
echo ${num// / + }
10 + 20 + -30
But result will stay same:
echo $(( ${num// / + } ))
0
sum=$num | sed -e 's/-/+/g'
With respect to what's present above, sum=$num and sed become two different commands. It's not grouped together as you wanted, which makes the sed ineffective.
Also, you'd need to echo $num
Solution is to group them together, like:
sum=`echo $num | sed -e 's/-/+/g`
OR
sum=$(echo $num | sed -e 's/-/+/g')
OR Rather, an alternate approach
sum=${num//-/+}

How to loop through a range of characters in a bash script using ASCII values?

I am trying to write a bash script which will read two letter variables (startletter/stopletter) and after that I need to print from the start letter to the stop letter with a for or something else. How can I do that?
I tried to do
#! /bin/bash
echo "give start letter"
read start
echo "give stop letter" read stop
But none of the for constructs work
#for value in {a..z}
#for value in {$start..$stop}
#for (( i=$start; i<=$stop; i++)) do echo "Letter: $c" done
This question is very well explained in BashFAQ/071 How do I convert an ASCII character to its decimal (or hexadecimal) value and back?
# POSIX
# chr() - converts decimal value to its ASCII character representation
# ord() - converts ASCII character to its decimal value
chr () {
local val
[ "$1" -lt 256 ] || return 1
printf -v val %o "$1"; printf "\\$val "
# That one requires bash 3.1 or above.
}
ord() {
# POSIX
LC_CTYPE=C printf %d "'$1"
}
Re-using them for your requirement, a proper script would be written as
read -p "Input two variables: " startLetter stopLetter
[[ -z "$startLetter" || -z "$stopLetter" ]] && { printf 'one of the inputs is empty\n' >&2 ; }
asciiStart=$(ord "$startLetter")
asciiStop=$(ord "$stopLetter")
for ((i=asciiStart; i<=asciiStop; i++)); do
chr "$i"
done
Would print the letters as expected.
Adding it to community-wiki since this is also a cross-site duplicate from Unix.SE - Bash script to get ASCII values for alphabet
In case you feel adventurous and want to use zsh instead of bash, you can use the following:
For zsh versions below 5.0.7 you can use the BRACE_CCL option:
(snip man zshall) If a brace expression matches none of the above forms, it is left
unchanged, unless the option BRACE_CCL (an abbreviation for 'brace character class') is set. In that case, it is expanded to a list of the individual characters between the braces sorted into the order of the characters in the ASCII character set (multibyte characters are not currently handled). The syntax is similar to a [...] expression in filename generation: - is treated specially to denote a range of characters, but ^ or ! as the first character is treated normally. For example, {abcdef0-9}
expands to 16 words 0 1 2 3 4 5 6 7 8 9 a b c d e f.
#!/usr/bin/env zsh
setopt brace_ccl
echo "give start letter"
read cstart
echo "give stop letter"
read cstop
for char in {${cstart}-${cstop}}; do echo $char; done
For zsh versions from 5.0.7 onwards you can use the default brace expansion :
An expression of the form {c1..c2}, where c1 and c2 are single characters (which may be multibyte characters), is expanded to every character in the range from c1 to c2 in whatever character sequence is used internally. For characters with code points below 128 this is US ASCII (this is the only case most users will need). If any intervening character is not printable, appropriate quotation is used to render it printable. If the character sequence is reversed, the output is in reverse order, e.g. {d..a} is substituted as d c b a.
#!/usr/bin/env zsh
echo "give start letter"
read cstart
echo "give stop letter"
read cstop
for char in {${cstart}..${cend}; do echo $char; done
More information on zsh can be found here and the quick reference

shell script - is there any way converting number to char? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Integer ASCII value to character in BASH using printf
I want to convert my integer number to ASCII character
We can convert in java like this:
int i = 97; //97 is "a" in ASCII
char c = (char) i; //c is now "a"
But,is there any way in to do this shell scripting?
#!/bin/bash
# chr() - converts decimal value to its ASCII character representation
# ord() - converts ASCII character to its decimal value
chr() {
printf \\$(printf '%03o' $1)
}
ord() {
printf '%d' "'$1"
}
ord A
echo
chr 65
echo
Edit:
As you see ord() is a little tricky -- putting a single quote in front of an integer.
The Single Unix Specification: "If the leading character is a
single-quote or double-quote, the value shall be the numeric value in
the underlying codeset of the character following the single-quote or
double-quote."
(Taken from http://mywiki.wooledge.org/BashFAQ/071).
See man printf(1p).
declare -i i=97
c=$(printf \\$(printf '%03o' $i))
echo "char:" $c

SPRINTF in shell scripting?

I have an auto-generated file each day that gets called by a shell script.
But, the problem I'm facing is that the auto-generated file has a form of:
FILE_MM_DD.dat
... where MM and DD are 2-digit month and day-of-the-month strings.
I did some research and banged it at on my own, but I don't know how to create these custom strings using only shell scripting.
To be clear, I am aware of the DATE function in Bash, but what I'm looking for is the equivalent of the SPRINTF function in C.
In Bash:
var=$(printf 'FILE=_%s_%s.dat' "$val1" "$val2")
or, the equivalent, and closer to sprintf:
printf -v var 'FILE=_%s_%s.dat' "$val1" "$val2"
If your variables contain decimal values with leading zeros, you can remove the leading zeros:
val1=008; val2=02
var=$(printf 'FILE=_%d_%d.dat' $((10#$val1)) $((10#$val2)))
or
printf -v var 'FILE=_%d_%d.dat' $((10#$val1)) $((10#$val2))
The $((10#$val1)) coerces the value into base 10 so the %d in the format specification doesn't think that "08" is an invalid octal value.
If you're using date (at least for GNU date), you can omit the leading zeros like this:
date '+FILE_%-m_%-d.dat'
For completeness, if you want to add leading zeros, padded to a certain width:
val1=8; val2=2
printf -v var 'FILE=_%04d_%06d.dat' "$val1" "$val2"
or with dynamic widths:
val1=8; val2=2
width1=4; width2=6
printf -v var 'FILE=_%0*d_%0*d.dat' "$width1" "$val1" "$width2" "$val2"
Adding leading zeros is useful for creating values that sort easily and align neatly in columns.
Why not using the printf program from coreutils?
$ printf "FILE_%02d_%02d.dat" 1 2
FILE_01_02.dat
Try:
sprintf() { local stdin; read -d '' -u 0 stdin; printf "$#" "$stdin"; }
Example:
$ echo bar | sprintf "foo %s"
foo bar

Resources