在脚本中设置“LANG=C LC_ALL=C”对 printf 中非英文字符的填充长度没有影响

在脚本中设置“LANG=C LC_ALL=C”对 printf 中非英文字符的填充长度没有影响

我正在将 Bash 脚本的输出格式化为表格,以便用于表示每列边界的管道字符相对于该列中最长的字符串间隔开(从而很好地排列所有管道字符)。

以下是相关的输出。请注意,对于“il2”和“ksp”行,两个竖线字符都是对齐的,但是在包含非英语字符“ý”的“iceland”行中,第二个竖线字符相对于其他行向左缩进一个空格。

iceland          | Mýrdalssandur Iceland                    | steam://rungameid/1248990                                                                   | iceland-Win64-Shipping.exe
il2              | IL 2 Sturmovik: 1946                      | steam://rungameid/15320                                                                     | il2fb.exe
ksp              | Kerbal Space Program                      | steam://rungameid/220200                                                                    | KSP_x64.exe

在脚本的最开始,我有以下内容:

oLang=$LANG oLcAll=$LC_ALL
LANG=C LC_ALL=C

最后,在最后的“完成”语句之前,我有:

LANG=$oLang LC_ALL=$oLcAll

整个脚本中没有其他语言设置的改变。

如果在 shell 中我将$string变量设置为“Mýrdalssandur Iceland”并运行printf "%-14s is %2d char length\n" "'$string'" ${#string},我会得到一个字符长度 21,但是当我运行LANG=C LC_ALL=C然后时printf "%-14s is %2d char length\n" "'$string'" ${#string},我会得到一个字符长度 22,这表明它实际上应该有所不同。

我创建了一个基本脚本来测试填充,并且在该脚本中,更改LANG确实产生了预期的效果,即使这些printf行包含在函数中,因为它们在所讨论的主脚本中。

我现在不明白为什么输出没有应用语言更改,因此感谢任何帮助。

以下是完整脚本:

#!/bin/bash

oLang=$LANG oLcAll=$LC_ALL
LANG=C LC_ALL=C

#Config file location
listpath="/home/$USER/.config/play/"
config="$listpath"config
games="$listpath"games.list

if ! [ -f $config ] ; then
    echo "Config file not found at \"$config\""
    exit
fi

#Vars/arrs used for script
gamecmd="$1"
prgm=""
line=""
pathf=""
ctr=0
rctr=0
appID=0
taskname=""
result=""
task=""
answer=""
longestshort=0
longestlong=0
longestpathf=0
longestservicename=0
ip=`cat $config | sed -n -e 's/^.*ip=//p'`
wr=`cat $config | sed -n -e 's/^.*wr=//p'`
display=`cat $config | sed -n -e 's/^.*display=//p'`
ControlMyMonitor=`cat $config | sed -n -e 's/^.*ControlMyMonitor=//p'`
ControlMyMonitor=${ControlMyMonitor//\\//}
declare -a short
declare -a long
declare -a path
declare -a servicename

#Shortcut locations to make adding games easier
STEAM="steam://rungameid/"
DESKTOP=`cat $config | sed -n -e 's/^.*desktop=//p'`
APPDATA=`cat $config | sed -n -e 's/^.*appdata=//p'`

function check_for_duplicate()
{
    dctr=0

    #Set delimiter to comma
    IFS=','

    while IFS= read -r line
    do
        read -a strarr <<< "$line"

        for (( n=0; n < ${#strarr[*]}; n++))
        do
            if [ "${strarr[n]}" == "$1" ]
            then
                get_record $dctr "$line"
                echo -e "Duplicate entry for \""$1"\" found:"
                echo
                print_verbose_header
                echo
                pathf="${path[$dctr]}"
                format_pathf
                    print_verbose_record "${short[$dctr]}" "${long[$dctr]}" "$pathf" "${servicename[$dctr]}"

                exit
            fi
        done
        ((dctr++))
    done < $games
}
function format_pathf()
{
    pathf="${pathf//\"/}"
    pathf="${pathf//%STEAM%/$STEAM}"
    pathf="${pathf//%DESKTOP%/$DESKTOP}"
    pathf="${pathf//%APPDATA%/$APPDATA}"
}
function get_normal_header_spacing()
{
    for (( IFS=" " i=0; i<$ctr; i++ ))
    do
        if [ "${#short[i]}" -gt "$longestshort" ]
        then
            longestshort="${#short[i]}"
        fi
        if [ "${#long[i]}" -gt "$longestlong" ]
        then
            longestlong="${#long[i]}"
        fi
        done
}
function get_verbose_header_spacing()
{
    for (( IFS=" " i=0; i<$ctr; i++ ))
    do
        if [ "${#short[i]}" -gt "$longestshort" ]
        then
            longestshort="${#short[i]}"
        fi
        if [ "${#long[i]}" -gt "$longestlong" ]
        then
            longestlong="${#long[i]}"
        fi

        pathf="${path[i]}"
        format_pathf

        if [ "${#pathf}" -gt "$longestpathf" ]
        then
            longestpathf="${#pathf}"
        fi
        if [ "${#servicename[i]}" -gt "$longestservicename" ]
        then
            longestservicename="${#servicename[i]}"
        fi
        done
}
function get_record()
{
    short[$1]=$(echo "$2" | grep -Po '([^,]+)' | head -1)
    long[$1]=$(echo "$2" | grep -Po '([^,]+)' | head -2 | tail -1)
    path[$1]=$(echo "$2" | grep -Po '([^,]+)' | head -3 | tail -1)
    servicename[$1]=$(echo "$2" | grep -Po '([^,]+)' | head -4 | tail -1)
}
function print_normal_header()
{
    printf "%-${longestshort}s   %-${longestlong}s\n" "Command" "Game Title"

    for (( i=0; i<$(($longestshort + $longestlong + 2)); i++ ))
    do
        echo -n "-"
    done
}
function print_verbose_header()
{
    printf "%-${longestshort}s   %-${longestlong}s   %-${longestpathf}s   %s\n" "Command" "Game Title" "Path" "Task Name"
        
    for (( i=0; i<$(($longestshort + $longestlong + $longestpathf + $longestservicename + 4)); i++ ))
    do
        echo -n "-"
    done
}
function print_normal_record()
{
    printf "%-${longestshort}s | %-${longestlong}s\n" "$1" "$2"
}
function print_verbose_record()
{
    printf "%-${longestshort}s | %-${longestlong}s | %-${longestpathf}s | %s\n" "$1" "$2" "$3" "$4"
}
function print_help()
{
    echo -e "Launch program on remote Windows PC and change monitor inputs automatically.\n"
    echo -e "USAGE"
    echo -e "-----"
    echo -e
    echo -e "\e[3mplay [COMMAND]\e[0m"
    echo -e "Launch the game defined by the COMMAND parameter.\n"
    echo -e "\e[3mplay -l\e[0m"
    echo -e "Print a formatted table of games saved to the games.list file excluding file paths.\n"
    echo -e "\e[3mplay -lv\e[0m"
    echo -e "Print a formatted table of games saved to the games.list file including file paths and task names.\n"
    echo -e "\e[3mplay -f [PATTERN]\e[0m"
    echo -e "Search for a matching PATTERN in the games.list file and list results excluding file paths.\n"
    echo -e "\e[3mplay -fv [PATTERN]\e[0m"
    echo -e "Search for a matching PATTERN in the games.list file and list results including file paths and task names.\n"
    echo -e "\e[3mplay -a [COMMAND] [TITLE] [PATH] [TASK]\e[0m"
    echo -e "Add a game to the list that is located at the PATH on the Windows machine, is started by using the COMMAND parameter, and can be easily referenced in the -l functionality by the game's formal TITLE, which shows up in task manager as TASK (typically a \".exe\" file).\n"
    echo -e "\tFilepath shortcuts for PATH"
    echo -e "\t---------------------------"
    printf  "\t%-10s %-1s %-50s\n" "%STEAM%" "=" "$STEAM"
    printf  "\t%-10s %-1s %-50s\n" "%DESKTOP%" "=" "$DESKTOP"
    printf  "\t%-10s %-1s %-50s\n\n" "%APPDATA%" "=" "$APPDATA"
    echo -e "\e[3mplay -r [PATTERN]\e[0m"
    echo -e "Search for a matching PATTERN in the games.list file and remove it from the games.list file. This will generate a games.list.bak file, which is the games.list file before the specified program is removed.\n"
    echo -e "\e[3mplay -?\e[0m OR \e[3mplay --help\e[0m"
    echo -e "Print this help text.\n"
}
function list_games()
{
        for (( IFS=" " i=0; i<$ctr; i++ ))
    do
        print_normal_record "${short[i]}" "${long[i]}"
        done
    echo
}
function list_games_verbose()
{
        for (( IFS=" " i=0; i<$ctr; i++ ))
    do
        #Remove/replace backslash, quote marks, and path shortcuts from path and print result in table
        pathf="${path[i]//\\/}"
        format_pathf
            print_verbose_record "${short[i]}" "${long[i]}" "$pathf" "${servicename[i]}"
        done
    echo
}
function run_game()
{
    isRunning=0

    echo "Changing monitor input and launching winrun with parameters \"start $1\""
    sudo chmon $display
    winrun "start $1"

    while [[ 1 ]]
    do
        #Wait for program to actually show up in Task Manager (accounts for user messing around in launcher
        if [ -n $(winrun "tasklist /fi \"SESSIONNAME eq CONSOLE\" /fo list" | grep -i $task) ] && [!isRunning]; then
            isRunning=1

        #Switch monitor inputs when program is no longer running
        elif [ -z $(winrun "tasklist /fi \"SESSIONNAME eq CONSOLE\" /fo list" | grep -i $task) ] && [isRunning]; then
            echo "No longer seeing $task running on Window system, changing display input."
            winrun "run $ControlMyMonitor /SetValue \\\\.\DISPLAY1\Monitor0 60 4"
            break;
        fi
    done
}

#Load games from games.list file
if [ -f $games ]
then

    #Read games list
    while IFS= read -r line
    do
        short[$ctr]=$(echo $line | awk -F, '{print $1}')
        long[$ctr]=$(echo $line | awk -F, '{print $2}')
        path[$ctr]=$(echo $line | awk -F, '{print $3}')
        servicename[$ctr]=$(echo $line | awk -F, '{print $4}')
        ((ctr++))
    done < $games
else
    #Make games list file if it does not exist
    echo $games file  not found, creating!
    touch $games
fi

#Check for script arguments
while [ "$#" != "" ]
do
    case "$1" in
        -l)#List games

            if [ "$2" ]
            then
                echo -e "Detected arguments after \""$1"\". Ignoring, as these are not used...\n"
            fi

            get_normal_header_spacing
            print_normal_header
            list_games | sort
            break;;

        -lv)#Display games list with more info
            
            if [ "$2" ]
            then
                echo -e "Detected arguments after \""$1"\". Ignoring, as these are not used...\n"
            fi

            get_verbose_header_spacing
                        print_verbose_header
            list_games_verbose | sort
                        break;;

        -f)#Find for parameter and display

            if [ ! "$2" ]
            then
                echo "Missing search parameter!" >&2
                break
            fi
            if [ "$3" ]
            then
                echo -e "Detected arguments after \""$2"\". Ignoring, as these are not used...\n"
            fi

            get_normal_header_spacing
            print_normal_header
            echo
            list_games | grep -i "$2" | sort
            break;;

        -fv)#Find for parameter and display with more info

            if [ ! "$2" ]
            then
                echo "Missing search parameter!" >&2
                break
            fi
            if [ "$3" ]
            then
                echo -e "Detected arguments after \""$2"\". Ignoring, as these are not used...\n"
            fi

            get_verbose_header_spacing
            print_verbose_header
            echo
            list_games_verbose | grep -i "$2" | sort
            break;;

        -a)#Add game to list

            if [ ! "$2" ]
            then
                echo "Missing program command string!" >&2
                break

                elif [ ! "$3" ]
                then
                    echo "Missing program title!" >&2
                    break

                    elif [ ! "$4" ]
                    then
                        echo "Missing program path!" >&2
                        break

                        elif [ ! "$5" ]
                        then
                            echo "Missing program task name!" >&2
                            break
            fi
            if [ "$6" ]
            then
                echo -e "Detected arguments after \""$5"\". Ignoring, as these are not used...\n"
            fi

            #Check for duplicate entry
            check_for_duplicate "$2"
            check_for_duplicate "$3"
            check_for_duplicate "$4"

            #If no duplicate is found, add entry
            if [ ! $match ]
            then
                pathf="${4//\\//}"
                echo ""$2","$3","$pathf","$5"" >> "$games"
                echo "Added the following record:"
                echo
                print_verbose_header
                echo
                format_pathf
                    print_verbose_record "$2" "$3" "$pathf" "$5"
            fi

            break;;

        -r)#Attempt to remove game from list

            if [ ! "$2" ]
            then
                echo "Missing search parameter!" >&2
                break
            fi
            if [ "$3" ]
            then
                echo -e "Detected arguments after \""$2"\". Ignoring, as these are not used...\n"
            fi

            rctr=0
            while IFS= read -r line
            do
                if [[ -n $(echo $line | grep "$2") ]]
                then
                    match=1

                    get_record $rctr "$line"
                    echo "Discovered the following matching record:"
                    echo
                    print_verbose_header
                    echo
                    pathf="${path[$rctr]}"
                    format_pathf
                        print_verbose_record "${short[$rctr]}" "${long[$rctr]}" "$pathf" "${servicename[$rctr]}"
                    echo                    

                    while [[ $answer != "y" && $answer != "n" ]]
                    do
                        echo -n "Remove this record? (y/n) "
                        read -r answer </dev/tty
                    done

                    if [ $answer == "n" ]
                    then
                        echo "$games not changed."
                        break
                    fi

                    echo
                    echo "Removed the following record:"
                    print_verbose_header
                    echo
                    print_verbose_record "${short[$rctr]}" "${long[$rctr]}" "$pathf" "${servicename[$rctr]}"

                    #Shift all entries beyond the removed entry down one
                    for (( i=$rctr; i<ctr; i++ ))
                    do
                        short[i]=${short[i+1]}                      
                        long[i]=${long[i+1]}
                        path[i]=${path[i+1]}
                        servicename[i]=${servicename[i+1]}
                    done

                    #Rebuild games.list
                    cp $games $games.bak
                    rm $games
                    for (( i=0; i<(ctr-1); i++ ))
                    do
                        echo "${short[i]}","${long[i]}","${path[i]}","${servicename[i]}" >> $games
                    done

                    break
                fi

                ((rctr++))
            done < $games


            if [ ! $match ]
            then
                echo -e "Did not find match for \""$2"\" in \"$games\""
            fi
            break;;

        -? | --help)#Print help text

            if [ "$2" ]
            then
                echo -e "Detected arguments after \""$1"\". Ignoring, as these are not used...\n"
            fi

            print_help
            break;;

        *)#Search for matching game

            if [ ! "$1" ]
            then
                print_help
                break;
            fi

            if [ "$2" ]
            then
                echo -e "Detected arguments after \""$1"\". Ignoring, as these are not used...\n"
            fi

            echo -n "Checking if \""$1"\" matches a known game in games.list... "
            shopt -s nocasematch
            for (( i=0; i<$ctr; i++ ))
            do
                if [[ "$1" == "${short[i]}" ]]; then
                    path[i]="${path[i]//%STEAM%/$STEAM}"
                    path[i]="${path[i]//%DESKTOP%/$DESKTOP}"
                    path[i]="${path[i]//%APPDATA%/$APPDATA}"
                    prgm="${path[i]}"
                    task="${servicename[i]}"
                    break
                fi
            done
            shopt -u nocasematch

            #If there was a match, run program. Otherwise, run first arg as is.
            if [ "$prgm" != "" ]
            then
                echo "Found!"
                run_game "$prgm"
            else
                echo "Not found!"
                run_game "$1"
            fi

            break;;
    esac
    shift

#LANG=$oLang LC_ALL=$oLcAll

done

答案1

Bash 的printf字段宽度以字节为单位,不是以字符为单位。由于ý在 UTF-8 编码中存储为两个字节,因此 printf 会认为它是两列宽——无论您的语言环境设置如何。

(对,就那个与字符串操作不一致${#var},很可能是因为该printf命令只是直接传递给 C printf() 函数,而该函数始终是面向字节的,并且 bash 对其工作方式没有任何发言权。)

然而,如果printf 处理字符,那么 LC_CTYPE=C 会适得其反——你会两个字节c3 bd被视为代表单个字符,就像它们在单个单元格中绘制一样,因此您需要将 LC_CTYPE 设置为something.UTF-8。 (如果 bash 认为字符串比实际显示的宽,那么它将添加较少的填充,因此列看起来会比应有的窄,就像您的情况一样。)

我建议使用column大多数 Linux 系统自带的工具 - 它能够理解 UTF-8 输入,并且可以让您免于计算列宽。事实上,它甚至可以处理宽度超过 1 个单元格的字符 - 例如:

(
    printf '%s\t%s\t%s\t%s\n' "iceland" "Mýrdalssandur Iceland" "steam://rungameid/1248990" "iceland-Win64-Shipping.exe"
    printf '%s\t%s\t%s\t%s\n' "il2" "IL 2 Sturmovik: 1946" "steam://rungameid/15320" "il2fb.exe"
    printf '%s\t%s\t%s\t%s\n' "ksp" "Kerbal Sp

相关内容