我正在将 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