|
| 1 | +#!/usr/bin/env bash |
| 2 | +# @Function |
| 3 | +# Convert unix time to human readable date string. |
| 4 | +# Note: The range of the 10-digit unix time in second include recent date: |
| 5 | +# 9999999999: 2286-11-20 17:46:39 +0000 |
| 6 | +# 1000000000: 2001-09-09 01:46:40 +0000 |
| 7 | +# 0: 1970-01-01 00:00:00 +0000 |
| 8 | +# -1000000000: 1938-04-24 22:13:20 +0000 |
| 9 | +# -9999999999: 1653-02-10 06:13:21 +0000 |
| 10 | +# |
| 11 | +# @Usage |
| 12 | +# # default treat first 10 digits as second(include recent date) |
| 13 | +# $ uxt 1234567890 # unix time of second |
| 14 | +# 2009-02-14 07:31:30 +0800 |
| 15 | +# $ uxt 1234567890333 # unix time of milliseconds(10 + 3 digits) |
| 16 | +# 2009-02-14 07:31:30.333 +0800 |
| 17 | +# $ uxt 12345678903 # unix time of 10 + 1 digits |
| 18 | +# 2009-02-14 07:31:30.3 +0800 |
| 19 | +# # support multiply arguments |
| 20 | +# $ uxt 0 1234567890 12345678903 |
| 21 | +# |
| 22 | +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-unix-time |
| 23 | +# @author Jerry Lee (oldratlee at gmail dot com) |
| 24 | +set -eEuo pipefail |
| 25 | + |
| 26 | +readonly PROG=${0##*/} |
| 27 | +readonly PROG_VERSION='2.x-dev' |
| 28 | + |
| 29 | +################################################################################ |
| 30 | +# util functions |
| 31 | +################################################################################ |
| 32 | + |
| 33 | +red_print() { |
| 34 | + # if stdout is a terminal, turn on color output. |
| 35 | + # '-t' check: is a terminal? |
| 36 | + # check isatty in bash https://stackoverflow.com/questions/10022323 |
| 37 | + if [ -t 1 ]; then |
| 38 | + printf "\e[1;31m%s\e[0m\n" "$*" |
| 39 | + else |
| 40 | + printf '%s\n' "$*" |
| 41 | + fi |
| 42 | +} |
| 43 | + |
| 44 | +is_integer() { |
| 45 | + [[ "$1" =~ ^-?[[:digit:]]+$ ]] |
| 46 | +} |
| 47 | + |
| 48 | +die() { |
| 49 | + local prompt_help=false exit_status=2 |
| 50 | + while (($# > 0)); do |
| 51 | + case "$1" in |
| 52 | + -h) |
| 53 | + prompt_help=true |
| 54 | + shift |
| 55 | + ;; |
| 56 | + -s) |
| 57 | + exit_status=$2 |
| 58 | + shift 2 |
| 59 | + ;; |
| 60 | + *) |
| 61 | + break |
| 62 | + ;; |
| 63 | + esac |
| 64 | + done |
| 65 | + |
| 66 | + (($# > 0)) && red_print "$PROG: $*" |
| 67 | + $prompt_help && echo "Try '$PROG --help' for more information." |
| 68 | + |
| 69 | + exit "$exit_status" |
| 70 | +} >&2 |
| 71 | + |
| 72 | +usage() { |
| 73 | + cat <<EOF |
| 74 | +Usage: $PROG [OPTION] unix-time... |
| 75 | +
|
| 76 | +Convert unix time to human readable date string. |
| 77 | +Note: The range of the 10-digit unix time in second include recent date: |
| 78 | + 9999999999: 2286-11-20 17:46:39 +0000 |
| 79 | + 1000000000: 2001-09-09 01:46:40 +0000 |
| 80 | + 0: 1970-01-01 00:00:00 +0000 |
| 81 | + -1000000000: 1938-04-24 22:13:20 +0000 |
| 82 | + -9999999999: 1653-02-10 06:13:21 +0000 |
| 83 | +
|
| 84 | +Example: |
| 85 | + # default treat first 10 digits as second(include recent date) |
| 86 | + $ $PROG 1234567890 # unix time of second |
| 87 | + 2009-02-14 07:31:30 +0800 |
| 88 | + $ $PROG 1234567890333 # unix time of milliseconds(10 + 3 digits) |
| 89 | + 2009-02-14 07:31:30.333 +0800 |
| 90 | + $ $PROG 12345678903 # unix time of 10 + 1 digits |
| 91 | + 2009-02-14 07:31:30.3 +0800 |
| 92 | + # support multiply arguments |
| 93 | + $ $PROG 0 1234567890 12345678903 |
| 94 | +
|
| 95 | +Options: |
| 96 | + -u, --time-unit set the time unit of given epochs |
| 97 | + -Z, --no-time-zone do not print time zone |
| 98 | + -D, --no-second-decimal |
| 99 | + do not print second decimal |
| 100 | + -t, --trim-decimal-tailing-0 |
| 101 | + trim the tailing zeros of second decimal |
| 102 | + -h, --help display this help and exit |
| 103 | + -V, --version display version information and exit |
| 104 | +EOF |
| 105 | + |
| 106 | + exit |
| 107 | +} |
| 108 | + |
| 109 | +progVersion() { |
| 110 | + printf '%s\n' "$PROG $PROG_VERSION" |
| 111 | + exit |
| 112 | +} |
| 113 | + |
| 114 | +################################################################################ |
| 115 | +# parse options |
| 116 | +################################################################################ |
| 117 | + |
| 118 | +args=() |
| 119 | +unit= |
| 120 | +no_tz=false |
| 121 | +no_second_decimal=false |
| 122 | +trim_decimal_tailing_0=false |
| 123 | + |
| 124 | +while (($# > 0)); do |
| 125 | + case "$1" in |
| 126 | + -u | --unit) |
| 127 | + unit=$2 |
| 128 | + shift 2 |
| 129 | + ;; |
| 130 | + -Z | --no-time-zone) |
| 131 | + no_tz=true |
| 132 | + shift |
| 133 | + ;; |
| 134 | + -D | --no-second-decimal) |
| 135 | + no_second_decimal=true |
| 136 | + shift |
| 137 | + ;; |
| 138 | + -t | --trim-decimal-tailing-0) |
| 139 | + trim_decimal_tailing_0=true |
| 140 | + shift |
| 141 | + ;; |
| 142 | + -h | --help) |
| 143 | + usage |
| 144 | + ;; |
| 145 | + -V | --version) |
| 146 | + progVersion |
| 147 | + ;; |
| 148 | + -[[:digit:]]*) |
| 149 | + # negative number start with '-', is not option |
| 150 | + args=(${args[@]:+"${args[@]}"} "$1") |
| 151 | + shift |
| 152 | + ;; |
| 153 | + --) |
| 154 | + shift |
| 155 | + args=(${args[@]:+"${args[@]}"} "$@") |
| 156 | + break |
| 157 | + ;; |
| 158 | + -*) |
| 159 | + die -h "unrecognized option '$1'" |
| 160 | + ;; |
| 161 | + *) |
| 162 | + args=(${args[@]:+"${args[@]}"} "$1") |
| 163 | + shift |
| 164 | + ;; |
| 165 | + esac |
| 166 | +done |
| 167 | + |
| 168 | +[[ -n $unit ]] && if [[ $unit =~ ^(s|second)$ ]]; then |
| 169 | + unit=s |
| 170 | +elif [[ $unit =~ ^(ms|millisecond)$ ]]; then |
| 171 | + unit=ms |
| 172 | +else |
| 173 | + die -h "illegal time unit '$unit'! support values: 'second'/'s', 'millisecond'/'ms'" |
| 174 | +fi |
| 175 | + |
| 176 | +readonly args unit trim_decimal_tailing_0 no_tz |
| 177 | + |
| 178 | +((${#args[@]} > 0)) || die -h "requires at least one argument!" |
| 179 | +for a in "${args[@]}"; do |
| 180 | + is_integer "$a" || die "argument $a is not integer!" |
| 181 | + [[ ! $a =~ ^-?0+[1-9] ]] || die "argument $a contains beginning 0!" |
| 182 | +done |
| 183 | + |
| 184 | +################################################################################ |
| 185 | +# biz logic |
| 186 | +################################################################################ |
| 187 | + |
| 188 | +print_date() { |
| 189 | + local -r input=$1 |
| 190 | + # split input integer to sign and number part |
| 191 | + local -r sign_part=${input%%[!-]*} # remove digits from tail |
| 192 | + local -r number_part=${input#-} # remove sign from head |
| 193 | + local -r np_len=${#number_part} # length of number part |
| 194 | + |
| 195 | + local second_part=0 decimal_part= |
| 196 | + # case 1: is unix time in second? |
| 197 | + if [[ $unit = s ]]; then |
| 198 | + second_part=$number_part |
| 199 | + # case 2: is unix time in millisecond? |
| 200 | + elif [[ $unit = ms ]]; then |
| 201 | + if ((np_len > 3)); then |
| 202 | + second_part=${number_part:0:np_len-3} |
| 203 | + decimal_part=${number_part:np_len-3:3} |
| 204 | + else |
| 205 | + printf -v decimal_part '%03d' "$number_part" |
| 206 | + fi |
| 207 | + # case 3: auto detect by length |
| 208 | + else |
| 209 | + # <= 10 digits, treat as second |
| 210 | + if ((np_len <= 10)); then |
| 211 | + second_part=$number_part |
| 212 | + # for long integer(> 10 digits), treat first 10 digits as second, |
| 213 | + # and the rest as decimal/nano second(almost 9 digits) |
| 214 | + elif ((np_len <= 19)); then |
| 215 | + second_part=${number_part:0:10} |
| 216 | + decimal_part=${number_part:10:9} |
| 217 | + else |
| 218 | + die "argument $input contains $np_len digits(>19), too many to treat as a recent date(first 10-digits as seconds, rest at most 9 digits as decimal)" |
| 219 | + fi |
| 220 | + fi |
| 221 | + |
| 222 | + # trim tailing zeros of decimal? |
| 223 | + $trim_decimal_tailing_0 && while true; do |
| 224 | + local old_len=${#decimal_part} |
| 225 | + decimal_part=${decimal_part%0} |
| 226 | + ((${#decimal_part} < old_len)) || break |
| 227 | + done |
| 228 | + |
| 229 | + local -r seconds_value=$sign_part$second_part second_part decimal_part |
| 230 | + # defensive check. 9999999999999999(16 '9') seconds is so big, 300M years later(316,889,355-01-25 17:46:39 +0000) |
| 231 | + ((${#second_part} <= 16)) || |
| 232 | + die "argument $input(seconds: $seconds_value${decimal_part:+, decimal: .$decimal_part}) is too big, seconds are more than 16 digits." |
| 233 | + |
| 234 | + local date_input=$seconds_value${decimal_part:+.$decimal_part} |
| 235 | + local format_n= |
| 236 | + $no_second_decimal || format_n=${decimal_part:+.%${#decimal_part}N} |
| 237 | + local format_tz= |
| 238 | + $no_tz || format_tz=' %z' |
| 239 | + date -d "@$date_input" +"%Y-%m-%d %H:%M:%S$format_n$format_tz" |
| 240 | +} |
| 241 | + |
| 242 | +for a in "${args[@]}"; do |
| 243 | + print_date "$a" |
| 244 | +done |
0 commit comments