325 lines
8.7 KiB
Bash
Executable File
325 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# base.sh - convert decimal numbers from/to base 2, 8, 10 and 16.
|
|
#
|
|
# (C) Bruno Raoult ("br"), 2024
|
|
# Licensed under the GNU General Public License v3.0 or later.
|
|
# Some rights reserved. See COPYING.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with this
|
|
# program. If not, see <https://www.gnu.org/licenses/gpl-3.0-standalone.html>.
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later <https://spdx.org/licenses/GPL-3.0-or-later.html>
|
|
#
|
|
|
|
CMDNAME=${0##*/} # script name
|
|
|
|
# some default values (blocks separator padchar)
|
|
# Attention: For output base 10, obase is 1
|
|
declare -i ibase=0 obase=0 padding=0 prefix=1 ogroup=0 intbits
|
|
|
|
# find out int size (bits) - suppose 2-complement, and 8 bits char
|
|
printf -v _b "%x" -1
|
|
(( intbits = ${#_b} * 4 ))
|
|
|
|
declare -rA _bases=( # -f/-b accepted values
|
|
[2]=2 [b]=2 [B]=2
|
|
[8]=8 [o]=8 [O]=8
|
|
[10]=10 [d]=10 [D]=10
|
|
[16]=16 [h]=16 [H]=16
|
|
[a]=-1 [g]=-1
|
|
)
|
|
declare -A _pad=( # group separator
|
|
[2]=" " [8]=" " [10]="," [16]=" "
|
|
)
|
|
declare -rA _ogroup=( # group size
|
|
[2]=8 [8]=3 [10]=3 [16]=4
|
|
)
|
|
declare -rA _oprefix=( # output prefix
|
|
[2]="2#" [8]="0" [10]="" [16]="0x"
|
|
)
|
|
|
|
usage() {
|
|
printf "usage: %s [OPTIONS] [NUMBER]...\n" "$CMDNAME"
|
|
printf "Use '%s -h' for more help\n" "$CMDNAME"
|
|
}
|
|
|
|
help() {
|
|
cat << _EOF
|
|
usage: $CMDNAME [OPTIONS] [NUMBER]...
|
|
-f, --from=BASE input base, see BASE below. Default is "g"
|
|
-t, --to=BASE output base, see BASE below. Default is "a"
|
|
-b, -o, -d, -x equivalent to -t2, -t8, -t10, -t16"
|
|
-g, --group=[SEP] group output (see OUTPUT below)
|
|
-p, --padding 0-pad output on block boundary (implies -g)
|
|
-n, --noprefix remove base prefixes in output
|
|
-h, --help this help
|
|
-- end of options
|
|
|
|
$CMDNAME output the NUMBERS arguments in different bases. If no NUMBER is
|
|
given, standard input will be used.
|
|
|
|
BASE
|
|
2, b, B binary
|
|
8, o, O, 0 octal
|
|
10, d, D decimal
|
|
16, h, H, 0x hexadecimal
|
|
a, g all/any: Default, guess format for '-f', output all
|
|
bases for '-t'
|
|
|
|
INPUT NUMBER
|
|
If input base is not specified, some prefixes are supported.
|
|
'b' or '2/' for binary, '0', 'o' or '8/' for octal, '0x', 'x' or
|
|
'16/' for hexadecimal, and 'd' or '10/' for decimal.
|
|
If no above prefix is found, decimal is assumed.
|
|
Decimal input may be signed or unsigned, with limits imposed by current
|
|
Bash (here: $intbits bits).
|
|
|
|
OUTPUT
|
|
Decimal output is always unsigned.
|
|
By default, the input number is shown converted in the 4 supported
|
|
bases (16, 10, 8, 2, in this order), separated by one tab character.
|
|
Without '-n' option, all output numbers but decimal will be prefixed:
|
|
'2#' for binary, '0' for octal, '0x' for hexadecimal, making them
|
|
usable for input in some otilities such as bash(1).]
|
|
With '-g' option, number digits will be grouped by 3 (octal,
|
|
decimal), 4 (hexadecimal), or 8 (binary). If no SEP character is given,
|
|
the separator will be ',' (comma) for decimal, space otherwise.
|
|
This option may be useless for default output, with multiple numbers
|
|
on one line.
|
|
The '-p' option add 0 padding up to the base grouping boundary.
|
|
|
|
EXAMPLES
|
|
Converting number in hexadecimal, decimal, octal, and binary, with or without
|
|
prefixes. Here, '\t' separator is shown as space:
|
|
$ $CMDNAME 0
|
|
0x0 0 0 2#0
|
|
|
|
$ $CMDNAME 123456
|
|
0x1e240 123456 0361100 2#11110001001000000
|
|
|
|
$ $CMDNAME -n 2/100
|
|
4 4 4 100
|
|
|
|
$ $CMDNAME -n 0x1e240
|
|
1e240 123456 361100 11110001001000000
|
|
|
|
Binary output, no prefix, grouped output:
|
|
$ $CMDNAME -bng 0x1e240
|
|
1 11100010 01000000
|
|
|
|
Negative input (decimal only):
|
|
$ $CMDNAME -x -- -1
|
|
0xffffffffffffffff
|
|
|
|
Input base indication, left padding binary output, no prefix:
|
|
$ $CMDNAME -nbp -f8 361100
|
|
00000001 11100010 01000000
|
|
|
|
Set group separator. Note that the separator *must* immediately follow the '-g'
|
|
option, without spaces:
|
|
$ $CMDNAME -nxg: 123456
|
|
1:e240
|
|
|
|
Long options, with separator and padding:
|
|
$ $CMDNAME --to=16 --noprefix --padding --group=: 12345
|
|
0001:e240
|
|
|
|
TODO
|
|
Add option for signed/unsigned integer output.
|
|
Remove useless octal output ?
|
|
_EOF
|
|
}
|
|
|
|
zero_pad() {
|
|
local n="$1" str="$2"
|
|
|
|
printf "%0.*d%s" $(( n - ${#str} % n)) 0 "$str"
|
|
}
|
|
|
|
split() {
|
|
local base="$1" str="$2"
|
|
local res="$str" sep=${_pad[$base]}
|
|
local -i n=${_ogroup[$base]}
|
|
|
|
(( padding )) && str=$(zero_pad "${_ogroup[$base]}" "$str")
|
|
if (( ogroup )); then
|
|
res=""
|
|
while (( ${#str} )); do
|
|
if (( ${#str} <= n )); then # finished
|
|
res="${str}${res:+$sep$res}"
|
|
break
|
|
fi
|
|
res="${str: -n}${res:+$sep$res}"
|
|
str="${str:0:-n}"
|
|
done
|
|
fi
|
|
printf "%s" "$res"
|
|
}
|
|
|
|
bin() {
|
|
local str=""
|
|
local -i n dec="$1"
|
|
|
|
# take care of negative numbers, as >> operator keeps the sign.
|
|
# 'intbits' is size of integer in bits in current shell.
|
|
for (( n = 0 ; dec && (n < intbits); n++ )); do
|
|
str="$(( dec & 1 ))$str"
|
|
(( dec >>= 1 ))
|
|
done
|
|
printf "%s\n" "${str:-0}"
|
|
}
|
|
|
|
hex() {
|
|
printf "%lx" "$1"
|
|
}
|
|
|
|
dec() {
|
|
printf "%lu" "$1"
|
|
}
|
|
|
|
oct() {
|
|
printf "%lo" "$1"
|
|
}
|
|
|
|
declare -a args=()
|
|
|
|
parse_opts() {
|
|
# short and long options
|
|
local sopts="f:t:bodxg::pnh"
|
|
local lopts="from:,to:,group::,padding,noprefix,help"
|
|
# set by options
|
|
local tmp=""
|
|
|
|
if ! tmp=$(getopt -o "$sopts" -l "$lopts" -n "$CMDNAME" -- "$@"); then
|
|
usage
|
|
exit 1
|
|
fi
|
|
eval set -- "$tmp"
|
|
|
|
while true; do
|
|
case "$1" in
|
|
"-f"|"--from")
|
|
ibase=${_bases[$2]}
|
|
if (( ! ibase )); then
|
|
usage
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
"-t"|"--to")
|
|
obase=${_bases[$2]}
|
|
if (( ! obase )); then
|
|
usage
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
"-b") obase=2 ;;
|
|
"-o") obase=8 ;;
|
|
"-d") obase=1 ;;
|
|
"-x") obase=16 ;;
|
|
"-g"|"--group")
|
|
ogroup=1
|
|
if [[ -n "$2" ]]; then
|
|
for i in 2 8 10 16; do _pad["$i"]="$2"; done
|
|
fi
|
|
shift
|
|
;;
|
|
"-p"|"--padding") ogroup=1; padding=1 ;;
|
|
"-n"|"--noprefix") prefix=0 ;;
|
|
"-h"|"--help") help ; exit 0 ;;
|
|
"--") shift; break ;;
|
|
*) usage; echo "Internal error [$1]!" >&2; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
# next are numbers to convert, if any
|
|
if (($# > 0)); then
|
|
args=("$@")
|
|
fi
|
|
}
|
|
|
|
addprefix() {
|
|
local base="$1" number="$2" _prefix=""
|
|
if (( prefix )); then
|
|
if [[ $base != 8 || $number != "0" ]]; then
|
|
_prefix="${_oprefix[$base]}"
|
|
fi
|
|
fi
|
|
printf "%s%s" "$_prefix" "$number"
|
|
}
|
|
|
|
stripprefix() {
|
|
[[ $1 =~ ^(0x|b|o|d|x|.*/) ]]
|
|
printf "%s" "${1#"${BASH_REMATCH[1]}"}"
|
|
}
|
|
|
|
guessbase() {
|
|
local input="$1"
|
|
local -i base=0
|
|
if [[ $input =~ ^(b|2/) ]]; then
|
|
base=2
|
|
elif [[ $input =~ ^(0x|x|16/) ]]; then
|
|
base=16
|
|
elif [[ $input =~ ^(0|o|8/) ]]; then
|
|
base=8
|
|
elif [[ $input =~ ^(d|10/) ]]; then
|
|
base=10
|
|
fi
|
|
return $(( base ? base : 10 ))
|
|
}
|
|
|
|
doit() {
|
|
local number="$2" multi="" val inum
|
|
local -i base=$1 decval _obase=$obase
|
|
if (( base <= 0 )); then
|
|
guessbase "$number"
|
|
base=$?
|
|
fi
|
|
|
|
inum=$(stripprefix "$number")
|
|
# convert input value to decimal
|
|
(( base == 10 )) && (( decval = inum ))
|
|
(( base != 10 )) && (( decval = "$base#$inum" ))
|
|
|
|
# mask for desired output: 1=decimal, others are same as base
|
|
if (( ! _obase )); then
|
|
(( _obase = 1|2|8|16 ))
|
|
multi=$'\t'
|
|
fi
|
|
|
|
if (( _obase & 16 )); then
|
|
val=$(addprefix 16 "$(split 16 "$(hex $decval)")")
|
|
printf "%s%s" "$val" "$multi"
|
|
fi
|
|
if (( _obase & 1 )); then
|
|
val=$(addprefix 10 "$(split 10 "$(dec $decval)")")
|
|
printf "%s%s" "$val" "$multi"
|
|
fi
|
|
if (( _obase & 8 )); then
|
|
val=$(addprefix 8 "$(split 8 "$(oct $decval)")")
|
|
printf "%s%s" "$val" "$multi"
|
|
fi
|
|
if (( _obase & 2 )); then
|
|
val=$(addprefix 2 "$(split 2 "$(bin $decval)")")
|
|
printf "%s%s" "$val" "$multi"
|
|
fi
|
|
printf "\n"
|
|
}
|
|
|
|
parse_opts "$@"
|
|
|
|
if ! (( ${#args[@]} )); then
|
|
while read -era line; do
|
|
for input in "${line[@]}"; do
|
|
doit "ibase" "$input"
|
|
done
|
|
done
|
|
else
|
|
for input in "${args[@]}"; do
|
|
doit "$ibase" "$input"
|
|
done
|
|
fi
|
|
exit 0
|