4

I think this is simple, but it's not working for me. This is what I have.

float=$(awk -v res="$result" 'BEGIN {printf "%.2f", res / 1000}')

I want to use a variable to set the decimal value for %.2f but awk will have none of it. This is what I need.

var=2
float=$(awk -v res="$result" 'BEGIN {printf "%.${var}f", res / 1000}')

I hope someone can show me the error of my ways.

1
  • 1
    Please edit your question and add possible values for variable $result. Commented Jul 9 at 14:56

4 Answers 4

8

awk's printf, like C's, lets you use a * instead of a hardcoded width or precision number. The value is instead taken from the next argument to the function. So instead of messing with shell substitution into the awk script, you can just pass the precision as another variable:

$ result=2050
$ prec=2
$ awk -v res="$result" -v p="$prec" 'BEGIN { printf "%.*f\n", p, res/1000 }'
2.05

However, if you're going to do a lot with floating point numbers, I'd suggest moving away from bash to another shell like zsh or ksh that supports floating-point math natively, or to a non-shell scripting language (perl, tcl, ruby, etc.). Tends to be more efficient than having to run external programs to do basic things like division.

Sign up to request clarification or add additional context in comments.

Comments

3

I suggest:

result="123456"
var=2            # decimal places

awk -v res="$result" -v dp="$var" 'BEGIN {printf "%." dp "f", res / 1000}'

or

awk -v res="$result" -v CONVFMT="%.${var}f" 'BEGIN {printf res / 1000}'

Output:

123.46

From man awk:

CONVFMT: The conversion format for numbers, "%.6g", by default.

3 Comments

Alternative syntax with OFMT instead of CONVFMT : awk -v res="$result" -v OFMT="%.${var}f" 'BEGIN {print res / 1000}'
The OFMT approach fails if res / 1000 happens to be an integer ::: __res__='37000'; __var__='2'; awk -v res="$__res__" -v OFMT="%.${__var__}f" -- 'BEGIN { print res / 1000 }' ======> 37. I've tested it with gawk mawk-1 mawk-2 beta and nawk, and all yielded 37 not 37.00
if you really really prefer the OFMT approach, then the way to circumvent the issue would be :::::::::: :::::::::::: :::::::::::::: awk 'BEGIN { printf(OFMT, res / 1000) }'
2

If available/acceptable, using a pure bash solution with version 5.3 with the loadable builtin fltexpr and the new form of Command Substitution , and bash builtin printf with the -v option/flag

#!/usr/bin/env bash

enable fltexpr || exit

var=2
result=12345

LC_NUMERIC=C printf -v float '%.*f' "$var" "${ fltexpr -p "$result / 1000"; }"

echo "$float"

Or

#!/usr/bin/env bash

enable fltexpr || exit

var=2
result=12345

echo "${| LC_NUMERIC=C printf -v REPLY '%.*f' "$var" "${ fltexpr -p "$result / 1000"; }"; }"

Both should print/output

12.35

According to help fltexpr

fltexpr: fltexpr [-p] expression
    Evaluate floating-point arithmetic expression.

    Evaluate EXPRESSION as a floating-point arithmetic expression and,
    if the -p option is supplied, print the value to the standard output.

    Exit Status:
    If the EXPRESSION evaluates to 0, the return status is 1; 0 otherwise.

According to help printf

printf: printf [-v var] format [arguments]
    Formats and prints ARGUMENTS under control of the FORMAT.
    
    Options:
      -v var    assign the output to shell variable VAR rather than
                display it on the standard output
    
    FORMAT is a character string which contains three types of objects: plain
    characters, which are simply copied to standard output; character escape
    sequences, which are converted and copied to the standard output; and
    format specifications, each of which causes printing of the next successive
    argument.
    
    In addition to the standard format characters csndiouxXeEfFgGaA described
    in printf(3), printf interprets:
    
      %b        expand backslash escape sequences in the corresponding argument
      %q        quote the argument in a way that can be reused as shell input
      %Q        like %q, but apply any precision to the unquoted argument before
                quoting
      %(fmt)T   output the date-time string resulting from using FMT as a format
                string for strftime(3)
    
    The format is re-used as necessary to consume all of the arguments.  If
    there are fewer arguments than the format requires,  extra format
    specifications behave as if a zero value or null string, as appropriate,
    had been supplied.
    
    Exit Status:
    Returns success unless an invalid option is given or a write or assignment
    error occurs.

As per Léa Gris , added the LC_NUMERIC=C since it is affected by the systems locale

3 Comments

Very interesting... fltexpr. I'll look forward to it. However, none of the ultra-current rolling-releases (Archlinux or openSUSE Tumbleweed) have bash 5.3 yet. (still 5.2.37) :{
Hopefully they will catch up. The online manual now is 5.3 or at least reflects the features of 5.3
You must always set LC_NUMERIC=C before using printf with %f in Bash, otherwise it fails with an invalid number if the system locale uses a decimal delimiter other than a dot, like fr_FR. It can be set inline (no semicolon separator). The safe usage is: LC_NUMERIC=C printf -v float '%.*f' "$var" "${ fltexpr -p "$result / 1000"; }"
0

It happens because $var are not substituted between simple quotes ' If you want $var to be substituted by its value, 'BEGIN...' should be "BEGIN..." so that var is replaced.

But then, since you need actual double quotes " inside that BEGIN expression, you need to escape them

So

float=$(awk -v res="$result" "BEGIN {printf \"%.${var}f\", res / 1000}")

is probably what you were trying to do. Only difference with your attempt are the 2 I've described (use " to enclose expression. And then, escaping the \" around %.${var}f

It is better to avoid injection of bash variable or expression in the awk code, and deal with var as you already dealt with res/result.

I answered to your question "why it happens, were is the bug", and it happens because of the ' that prevented your $var injection (not mine :D) to work as you intended. So, I've shown that. And I usually try to keep as close as possible to initial code (otherwise, I could have solved your problem using totally different technique, and even skipping awk; that would solve your problem, but not answer your question)

Yet, I agree that this answer your "why" question, but is not the best solution to your problem.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.