Shell:如何从解析的数据(数字)制作文本模式条形图?

Sop*_*rez 5 bash charts

我正在开发一个 Linux 的 Bash shell 脚本,从只留下数字的文本文件中提取数据。这些是我的示例解析数据:

3
4
4
5
6
7
8
8
9
11
Run Code Online (Sandbox Code Playgroud)

我想创建一个像这样的简单文本模式条形图,但对应于这些值:

条形图

细节:

  • 我需要图形图表是垂直的
  • 第一个数字应该出现在左边,最新的数字应该出现在右边
  • A n(parsed number) characters high 列适合我。所以在我的例子中左边的第一个条应该是 3 个字符高,第二个 4,第三个 4,第四个 5,依此类推。

更准确地说,对于这个例子,一些东西(使用?字符)像:

         ?
         ?
        ?? 
       ???
      ????
     ?????
    ??????
   ???????
 ?????????
??????????
??????????
??????????
Run Code Online (Sandbox Code Playgroud)

请注意第一(左)列高 3 个字符,最后(右)列高 11 个字符。
带有$字符的相同示例,以使其更具可读性:

         $
         $
        $$
       $$$
      $$$$
     $$$$$
    $$$$$$
   $$$$$$$
 $$$$$$$$$
$$$$$$$$$$
$$$$$$$$$$
$$$$$$$$$$
Run Code Online (Sandbox Code Playgroud)

我所知道的最接近的是我的进度条方法,我在另一个脚本中使用过:

printf "\033[48;5;21m"   # Blue background color
for i in $(seq 1 $n); do printf " "; done   # Create bar using blue spaces
Run Code Online (Sandbox Code Playgroud)

这是:填充每行打印一个带有n空格的条。但是这个条是水平的,所以在这种情况下不合适。

我请求一些核心循环示例想法来创建此条形图。

在用户 Boardrider 的建议下,接受基于任何类 Unix 工具的解决方案。也接受基于 Linux shell 的脚本语言(如 Perl 或 Python)的解决方案,只要它们用于在许多设备上实现。

gil*_*dux 5

这是第一次和天真的尝试......这不是一个非常有效的解决方案,因为数据被多次解析,但可能会有所帮助。在某种程度上,这是@Walter_A建议的第一个循环想法。

#!/bin/sh
#
## building a vertical bar graph of data file
## /sf/ask/2165030871/
##
## 1. required. Data file with one value per line and nothing else!
## /!\ provide the (relative or absolute) file path, not file content
: ${1:?" Please provide a file name"}
test -e "$1" || { echo "Sorry, can't find $1" 1>&2 ; exit 2 ; }
test -r "$1" || { echo "Sorry, can't access $1" 1>&2 ; exit 2 ; }
test -f "$1" || { echo "Sorry, bad format file $1" 1>&2 ; exit 2 ; }
test $( grep -cv '^[0-9][0-9]*$' "$1" 2>/dev/null ) -ne 0 || { echo "Sorry, bad data in $1" 1>&2 ; exit 3 ; }
# setting characters
## 2. optional. Ploting character (default is Dollar sign)
## /!\ for blank color use "\033[48;5;21m \033[0m" or you'll mess...
c_dot="$2"
: ${c_dot:='$'}
## 3. optional. Separator characher (default is Dash sign)
## /!\ as Space is not tested there will be extra characters...
c_sep="$3"
: ${c_sep:='-'}
# init...
len_w=$(wc -l "$1" | cut -d ' ' -f 1 )
l_sep=''
while test "$len_w" -gt 0
do
        l_sep="${l_sep}${c_sep}";
        len_w=$(($len_w-1))
done
unset len_w
# part1: chart
echo ".${c_sep}${l_sep}${c_sep}."
len_h=$(sort -n "$1" | tail -n 1)
nbr_d=${#len_h}
while test "$len_h" -gt 0
do
        printf '| '
        for a_val in $(cat "$1")
        do
                test "$a_val" -ge "$len_h" && printf "$c_dot" || printf ' '
        done
        echo ' |'
        len_h=$(($len_h-1))
done
unset len_h
# part2: legend
echo "|${c_sep}${l_sep}${c_sep}|"
while test "$nbr_d" -gt 0
do
        printf '| '
        for a_val in $(cat "$1")
        do
                printf "%1s" $(echo "$a_val" | cut -c "$nbr_d")
        done
        echo ' |'
        nbr_d=$(($nbr_d-1))
done
unset nbr_d
# end
echo "'${c_sep}${l_sep}${c_sep}'"
unset c_sep
exit 0
Run Code Online (Sandbox Code Playgroud)

编辑 1:这是对脚本的返工。它纠正了分隔符处理(只需尝试使用“ ”或“|”作为要查看的第三个参数),但是当我使用参数 number 而不是附加变量时,它可能看起来不太可读。

编辑 2:它还处理负整数......你可以改变地面(第 5 个参数)

#!/bin/sh
#
## building a vertical bar graph of data file
## /sf/ask/2165030871/
##
## 1. required. Data file with one value per line and nothing else!
## /!\ provide the (relative or absolute) file path, not file content
: ${1:?" Please provide a file name"}
[ -e "$1" ] || { echo "Sorry, can't find $1" 1>&2 ; exit 2 ; }
[ -r "$1" ] || { echo "Sorry, can't access $1" 1>&2 ; exit 2 ; }
[ -f "$1" ] || { echo "Sorry, bad format file $1" 1>&2 ; exit 2 ; }
[ $( grep -cv '^[-0-9][0-9]*$' "$1" 2>/dev/null ) -ne 0 ] || { echo "Sorry, bad data in $1" 1>&2 ; exit 3 ; }
## /!\ following parameters should result to a single character
## /!\ for blank color use "\033[48;5;21m \033[0m" or you'll mess...
## 2. optional. Ploting character (default is Dollar sign)
## 3. optional. Horizontal border characher (default is Dash sign)
## 4. optional. Columns separator characher (default is Pipe sign)
## (!) however, when no arg provided the graph is just framed in a table
## 5. optional. Ground level integer value (default is Zero)
test "${5:-0}" -eq "${5:-0}" 2>/dev/null || { echo "oops, bad parameter $5" 1>&2 ; exit 3 ; }
# init...
_long=$(wc -l < "$1" ) # width : number of data/lines in file
if [ -n "$4" ]
then
        _long=$((_long*2-3))
fi
_line=''
while [ "$_long" -gt 0 ]
do
        _line="${_line}${3:--}"
        _long=$((_long-1))
done
unset _long
_from=$(sort -n "$1" | tail -n 1 ) # max int
_stop=$(sort -n "$1" | head -n 1 ) # min int
Run Code Online (Sandbox Code Playgroud)

这种返工有两种口味。第一个产生与前一个类似的输出。

# begin
echo "${4-.}${3:--}${_line}${3:--}${4-.}"
# upper/positive
if [ $_from -gt ${5:-0} ]
then
        while [ $_from -gt ${5:-0} ]
        do
                printf "${4:-| }"
                for _cint in $(cat "$1" )
                do
                        if [ $_cint -ge $_from ]
                        then
                                printf "${2:-$}$4"
                        else
                                printf " $4"
                        fi
                done
                echo " ${4:-|}"
                _from=$((_from-1))
        done
        echo "${4-|}${3:--}${_line}${3:--}${4-|}"
fi
unset _from
# center/legend
_long=$(wc -L < "$1" ) # height : number of chararcters in longuest line...
while [ $_long -ge 0 ]
do
        printf "${4:-| }"
        for _cint in $(cat "$1" )
        do
                printf "%1s$4" $(echo "$_cint" | cut -c "$_long" )
        done
        echo " ${4:-|}"
        _long=$((_long-1))
done
unset _long
# lower/negative
if [ $_stop -lt ${5:-0} ]
then
        _from=${5:-0}
        echo "${4-|}${3:--}${_line}${3:--}${4-|}"
        while [ $_from -gt $_stop ]
        do
                printf "${4:-| }"
                for _cint in $(cat "$1" )
                do
                        if [ $_cint -lt $_from ]
                        then
                                printf "${2:-$}$4"
                        else
                                printf " $4"
                        fi
                done
                echo " ${4:-|}"
                _from=$((_from-1))
        done
fi
unset _stop
# end
echo "${4-'}${3:--}${_line}${3:--}${4-'}"
exit 0
Run Code Online (Sandbox Code Playgroud)

注意:当所有值为正(高于地面)或负值(低于地面)时,有两个检查以避免额外的循环!好吧,也许我应该总是把“中心/图例”部分放在最后?当首先有正值和负值时,它看起来有点难看,而当只有负整数时,标签不读取相反并且带有令人不快的减号看起来很奇怪。
另请注意,这wc -L不是 POSIX ... ...因此可能需要另一个循环。

这是另一个变体,图例编号大小正确,而不是底部。这样做,我节省了一个额外的循环,但我真的不喜欢输出(我更喜欢左侧的值而不是右侧的值,但这是一种品味,不是吗?)

# begin
printf "${4-.}${3:--}${_line}${3:--}${4-.}"
# upper/positive
if [ $_from -gt ${5:-0} ]
then
        echo ""
        while [ $_from -gt ${5:-0} ]
        do
                _ctxt=''
                printf "${4:-| }"
                for _cint in $(cat "$1" )
                do
                        if [ $_cint -ge $_from ]
                        then
                                printf "${2:-$}$4"
                        else
                                printf " $4"
                        fi
                        if [ $_cint -eq $_from ]
                        then
                                _ctxt="_ $_from"
                        fi
                done
                echo " ${4:-}${_ctxt}"
                _from=$((_from-1))
        done
        _from=$((_from+1))
else
        echo "_ ${1}"
fi
# center/ground
if [ $_stop -lt ${5:-0} ] && [ $_from -gt ${5:-0} ]
then
        echo "${4-|}${3:--}${_line}${3:--}${4-|}_ ${1}"
fi
# lower/negative
if [ $_stop -lt ${5:-0} ]
then
        _from=${5:-0}
        while [ $_from -gt $_stop ]
        do
                _ctxt=''
                printf "${4:-| }"
                for _cint in $(cat "$1" )
                do
                        if [ $_cint -lt $_from ]
                        then
                                printf "${2:-$}$4"
                        else
                                printf " $4"
                        fi
                        if [ $_cint -eq $((_from-1)) ]
                        then
                                _ctxt="_ $_cint"
                        fi
                done
                echo " ${4:-|}${_ctxt}"
                _from=$((_from-1))
        done
fi
# end
unset _from
printf "${4-'}${3:--}${_line}${3:--}${4-'}"
if [ $_stop -lt ${5:-0} ]
then
        echo ""
else
        echo "_ ${1}"
fi
unset _stop
exit 0
Run Code Online (Sandbox Code Playgroud)

编辑 3:有一些额外的检查,所以当只有正数或负数时不会添加额外的地线。

最后,我认为最终的解决方案是两者的混合,其中值显示在侧面,而值的位置显示在中心。然后它更接近 GNU Plot 的输出。

# init...
_long=$(wc -l < "$1" )
if [ -n "$4" ]
then
        _long=$((_long*2-3))
fi
_line=''
while [ $_long -gt 0 ]
do
        _line="${_line}${3:--}"
        _long=$((_long-1))
done
unset _long
_from=$(sort -n "$1" | tail -n 1 ) # max int
_stop=$(sort -n "$1" | head -n 1 ) # min int
# begin
echo "${4-.}${3:--}${_line}${3:--}${4-.}"
# upper/positive
if [ $_from -gt ${5:-0} ]
then
        while [ $_from -gt ${5:-0} ]
        do
                _ctxt=''
                printf "${4:-| }"
                for _cint in $(cat "$1" )
                do
                        if [ $_cint -ge $_from ]
                        then
                                printf "${2:-$}$4"
                        else
                                printf " $4"
                        fi
                        if [ $_cint -eq $_from ]
                        then
                                _ctxt="_ $_from"
                        fi
                done
                echo " ${4:-|}$_ctxt"
                _from=$((_from-1))
        done
        echo "${4-|}${3:--}${_line}${3:--}${4-|}"
fi
# center/ground
_size=$(wc -l < "$1" ) # width : number of data/lines in file
##_long=${#_size} # height : number of chararcters in long
#_long=1
##while [ $_long -gt 0 ]
#while [ $_long -le ${#_size} ]
#do
       #_rank=1
       #printf "${4:-| }"
       #while [ $_rank -le $_size ]
       #do
               #printf "%1s$4" $( printf "%0${#_size}d" $_rank  | cut -c $_long )
               #_rank=$((_rank+1))
       #done
       #printf " ${4:-|}"
       ##_long=$((_long-1))
       #_long=$((_long+1))
       ##if [ $_long -eq 0 ]
       #if [ $_long -eq ${#_size} ]
       #then
               #printf "_ ${1}"
       #fi
       #echo ''
#done
_rank=1
printf "${4:-| }"
while [ $_rank -le $_size ]
do
        printf "%1s$4" $( expr "$_rank" : '.*\(.\)$' )
        _rank=$((_rank+1))
done
echo " ${4:-|}_ ${1}"
# lower/negative
if [ $_stop -lt ${5:-0} ]
then
        echo "${4-|}${3:--}${_line}${3:--}${4-|}"
        while [ $_from -gt $_stop ]
        do
                _ctxt=''
                printf "${4:-| }"
                for _cint in $(cat "$1" )
                do
                        if [ $_cint -lt $_from ]
                        then
                                printf "${2:-$}${4}"
                        else
                                printf " $4"
                        fi
                        if [ $_cint -eq $((_from-1)) ]
                        then
                                _ctxt="_ $_cint"
                        fi
                done
                echo " ${4:-|}$_ctxt"
                _from=$((_from-1))
        done
fi
unset _from
unset _stop
# end
echo "${4-'}${3:--}${_line}${3:--}${4-'}"
exit 0
Run Code Online (Sandbox Code Playgroud)

最后的改进将是扩展能力...