6

I used the following to convert from int to char and char to int in bash. But I do not understand how printf \\$(printf '%03o' $1) or printf '%d' "'$1" work. Please explain how printf \\$(printf '%03o' $1) and printf '%d' work.

#!/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
  • 1
    Don't use variables in the printf format string. Use printf '..%s..' "$foo". – Paulo Tomé Dec 13 '19 at 12:59
  • 2
    @PauloTomé, how would you do this with printf %s? – ilkkachu Dec 13 '19 at 13:17
  • @ilkkachu It was a generic answer. Would do printf \\$(printf '%03o' "$1"). The rationale is: printf interprets escape sequences and format specifiers in the format string. If variables are included, any escape sequences or format specifiers in the data will be interpreted too, when you most likely wanted to treat it as data – Paulo Tomé Dec 13 '19 at 13:23
  • 1
    @PauloTomé, isn't that what's in the question too? (apart from the missing quoting around "$1") With the quotes, the output of that inner printf should be a single octal number, and the function here relies on the outer printf interpreting that backslash escape, so using %s wouldn't work. – ilkkachu Dec 13 '19 at 15:10
  • @ilkkachu The main idea that I wanted to transmit was not to use variables in the printf format string. I've should have written: Don't use variables in the printf format string. Use printf "\\$(printf '%03o' "$1")". That would be more clear. – Paulo Tomé Dec 13 '19 at 15:22
8

printf '\101' where 101 is a an octal number outputs the byte with that value.

When sent to an ASCII terminal, that will be rendered as A as A is character 65 (octal 101) in ASCII and all ASCII-compatible character sets (which includes most modern charsets with the exception of the EBCDIC ones still used on some IBM systems).

In

printf \\$(printf '%03o' $1)

Which should have been written:

printf "\\$(printf '%03o' "$1")"

as leaving parameter expansions (like $1), or command substitution ($(...)) unquoted is the split+glob operator in Bourne-like shells which is not wanted here

  • printf '%03o' "$1" converts the number in $1 to a 3 digit octal
  • printf "\\$(...)" appends that octal to a \ (\\ inside double quotes becomes \) and passes that to printf so it will output the corresponding byte value.

Note that it only works in locales where the charset is one byte per character (like iso8859-1) or, in locales with a multi-byte charset, only for values 0 to 127.

In bash,

printf '%d\n' "'A"

prints the Unicode code-point of character A (or at least the value returned by mbtowc() which on GNU systems at least is the Unicode code-point).

Some other implementations (including the standalone GNU printf utility) instead return the value of the first byte of the character.

For ASCII characters like A and on ASCII-based systems, that doesn't make any difference, but for others it matters. For instance the Greek α character (U+03B1) is encoded as:

  • byte 225 in iso8859-7 (the standard Greek single-byte charset)
  • bytes 206 177 in UTF-8 (the most commonly used encoding of Unicode on Unix-like systems)
  • bytes 166 193 in GB18030 (the official Chinese encoding of Unicode).

Bash's printf '%d\n' "'α" will always output 945 (0x03b1 in hexadecimal), which is the Unicode code point of α regardless of the locale (at least on GNU systems), but others may return 225, 206 or 166 depending on the locale.

You can see from that those chr and ord are only the reverse of each other for ASCII characters (or values 0 to 127), or in locales using the iso8859-1 character set for all characters (values 0 to 255).

If ord() is meant to return the Unicode code point, then the reverse (print the character corresponding to a Unicode code point) would be:

chr() {
  printf "\U$(printf %08X "$1")"
}

(assuming bash 4.3 or above (\UXXXXXXXX was added in 4.2, but didn't work properly for characters U+0080 to U+00FF until 4.3)).

Then, in any locale:

$ ord α
945
$ chr 945
α

Or for ord() to return the values of the bytes of the encoding of a given character (in the current locale):

ord() {
  printf %s "$1" | od -An -vtu1
}

And for chr() to output those bytes:

chr() {
  printf "$(printf '\\%o' "$@")"
}

Then, in a UTF-8 locale for instance:

$ ord α
 206 177
$ chr 206 177
α

(your ord α would give 945, your chr would give garbage for both chr 945 and chr 206 177).

Or in a locale using iso8859-7:

$ ord α
 225
$ chr 225
α

(your ord α would give 945, though could give 225 if printf was replaced with /usr/bin/printf if on a GNU system).

4

The inner printf '%03o' $1 returns the value of $1 (i.e. 65) as octal value (65 -> 101).

The outer printf \\$(..) prints the character represented by the octal value.

See man printf line:

\NNN byte with octal value NNN (1 to 3 digits)

For printf '%d' "'$1" you need to specify ' to indicate that $1 should be treated as a single character constant, otherwise printf trows an error indicating the value is an invalid number. The value of the character constant is then being used by printf to print in decimal format "%d"

0

chr

printf \\$(printf '%03o' $1) # Should be ... printf "$(printf '\\%03o' "$1")"

The first printf use the output of the second to produce a character. We need to work backwards to decrypt it.

The second printf is used to convert an integer number (from $1) to an octal integer (the '%03o' format).

$ printf '%03o' 12
014
$ printf '%03o' 65
101
$ printf '%03o' 255
377

Then, that result is concatenated with the equivalent of an \ (it has to be doubled --or quoted-- to avoid its special meaning to the shell). The resulting string \101 is interpreted by printf as the octal value of a byte. But only up to three digits (in most shell implementations of printf). That interpretation of only three octal digits is because that type of format is intended to be used on single bytes 0-255 (or 0-377 in octal). So, most printf implementations convert octal numbers to only one byte. If that byte represents an ascii character in the locale being used by the console, that character will be printed out.

$ printf \\101\\n
A
$ printf \\377\\n
�

To print characters (not bytes) affected by the locale we need:

  • a printf capable of generating multibyte characters in the locale in use, and
  • some form of expressing that we want a character instead of a byte,

for example, in bash(4.3+):

$ printf \\U263A\\n
☺

Note that printf is able to print many digits:

$ printf '%03o\n' 1495195076287004671
122777777777777777777

But only the first three are accepted as an octal number:

$ printf '\122777777777777777777'
R777777777777777777                     # Note the R in the front.

ord

printf '%d' "'$1"

The data (not format) string gets the outer quotes removed and the resulting string start with a '. The strings that start with either a ' or a " are special to printf. For those strings, if the format is numeric, the value of the first character is printed (in the format given by the numeric format string):

$ printf '%d\n' '"A'      # decimal (doesn't expand quoted vars '"$var')
65
$ printf '%d\n' "'A"      # decimal
65
$ printf '%o\n' "'A"      # octal
101
$ printf '%x\n' "'A"      # Hexadecimal
41

Note that there is some variation for the "'…" interpretation of what is the first character among implementations (byte or character, and if byte, the result is affected by the locale used).

  • Wow! This is more than I expected! Thank you#! $1 is the parameter and %03o converts the input to 3 digit octal. I always wondered why and how octal is used. 1885 – 1885 Dec 14 '19 at 16:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.