Ben*_*son 6 linux language bash text-formatting
我正在将 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
Run Code Online (Sandbox Code Playgroud)
在脚本的最开始,我有以下内容:
oLang=$LANG oLcAll=$LC_ALL
LANG=C LC_ALL=C
Run Code Online (Sandbox Code Playgroud)
最后,就在最后的“完成”声明之前,我有:
LANG=$oLang LC_ALL=$oLcAll
Run Code Online (Sandbox Code Playgroud)
整个脚本中没有其他语言设置更改。
如果在 shell 中我将一个$string变量设置为 "Mýrdalssandur Iceland" 并运行printf "%-14s is %2d char length\n" "'$string'" ${#string},我得到的字符长度为 21,但是当我运行LANG=C LC_ALL=C和 then 时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
Run Code Online (Sandbox Code Playgroud)
use*_*686 14
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 Spce Prgram" "steam://rungameid/220200" "KSP_x64.exe"
printf '%s\t%s\t%s\t%s\n' "ffxiv" "???????????XIV(FF14)" "steam://rungameid/39210" "ffxiv.exe"
) | column -s $'\t' -t -N SHORT,LONG,URL,EXE -W LONG -o ' ? '
Run Code Online (Sandbox Code Playgroud)
花哨的选项喜欢-N或-o至少需要util-linux包的2.30 版,但基本的column -t -s ...仍然适用于旧版本。
但是如果你不想使用任何外部工具,那么你需要手动填充字符串——利用${#var}告诉你字符数的扩展(虽然不是终端单元的数量,因此不如column):
printf '%s%*s | %s%*s | %s%*s | %s%*s\n' \
"$short" $((maxshort - ${#short})) "" \
"$long" $((maxlong - ${#long})) "" \
"$url" $((maxurl - ${#url})) "" \
"$exe" $((maxexe - ${#exe})) "" ;
Run Code Online (Sandbox Code Playgroud)
注意:在上述两种情况下,无论您使用 'column' 还是 bash 的 ${#var},您都需要将数据解释为 UTF-8 以获得正确的字符数,因此您不能将 LANG 或 LC_ALL 设置为“ C”。