SC2046 – ShellCheck Wiki
See this page on GitHub
Problematic code:
Correct code:
# getfilename outputs 1 file
ls -l "$(getfilename)"
# getfilename outputs multiple files, linefeed separated
getfilename | while IFS='' read -r line
do
ls -l "$line"
done
Rationale:
When command expansions are unquoted, word splitting and globbing will occur. This often manifests itself by breaking when filenames contain spaces.
Trying to fix it by adding quotes or escapes to the data will not work. Instead, quote the command substitution itself.
If the command substitution outputs multiple pieces of data, use a loop instead.
Exceptions
In rare cases you actually want word splitting, such as in
# shellcheck disable=SC2046
gcc $(pkg-config --libs openssl) client.c
This is because pkg-config
outputs -lssl -lcrypto
, which you want to break up by spaces into -lssl
and -lcrypto
.
A bash alternative in these cases is to use read -a
for words or mapfile
for lines. ksh can also use read -a
, or a while read
loop for lines. In this case, since pkg-config
outputs words, you could use:
# Read words from one line into an array in bash and ksh, then expand each as args
read -ra args < <(pkg-config --libs openssl)
gcc "${args[@]}" client.c # expand as args
# Read lines into an array, then expand each as args
readarray -t file_args < <(find /etc/ -type f | grep some-check)
your-linter.sh "${file_args[@]}" # expand as args
ShellCheck is a static analysis tool for shell scripts. This page is part of its documentation.
Create an account to follow your favorite communities and start taking part in conversations.
Shellcheck is complaining about this with a «Quote this to prevent word splitting. [SC2046]». Any tips on how to structure this properly?
echo -e "${CLABEL}Ultimate FS layer: ${CLEAR}"$(stripDir "${pri}")
Specifically, it’s referencing the $ at the start of $(stripDir «${pri}»). If I get rid of the quotes around ${pri} and quote the whole $(stripdir ….), then it complains that the ${pri} needs to be quoted.
Around and around I go. Unable to see the forest through the trees.
About Community
A subreddit dedicated to bash scripting.
For bugs
- Rule Id:
SC2046
andSC2086
- My shellcheck version:
online
- I tried on shellcheck.net and verified that this is still a problem on the latest commit
- It’s not reproducible on shellcheck.net, but I think that’s because it’s an OS, configuration or encoding issue
Here’s a snippet or screenshot that shows the problem:
#!/bin/sh set -eu foo='ls -l' bar='/tmp/dir' mkdir -p -- "$bar" command -p $foo "$bar" command -p $(printf %s "$foo") "$bar"
Here’s what shellcheck currently says:
Line 10:
command -p $foo "$bar"
^-- SC2086: Double quote to prevent globbing and word splitting.
Line 11:
command -p $(printf %s "$foo") "$bar"
^-- SC2046: Quote this to prevent word splitting.
Here’s what I wanted or expected to see:
Nothing.
This show two variations of the same concept, when dynamically running commands with command
the actual command should not be quoted or it will result in an error like ls -l: command not found
. In cases like this the destination path should be not part of the command variable should be in quotes to account for cases when it contains spaces. Additionally the second variation with printf
seems to be required for ksh on my system.
There are other ways this do not rely on word splitting; consider using them instead. (Arrays, functions.)
It is perfectly valid to disable Shellcheck’s warnings; they exist to catch naïve errors, not to demand conformance.
With limited shells I do not have access to bash arrays and a function may work for this simplified example, but there are far more elaborate examples in scripts where a function would not help quite as easily.
Consider this.
#!/bin/sh
set -eu
cmd () {
command=; print=
while [ "$#" -gt 1 ]; do
var="$1"; shift
command="${command} $var"
case "$var" in
-- ) continue ;;
* ) print="${print} $var" ;;
esac
done
printf %s\n " $print '$1'"
command -p $(printf %s "${command#* }") "$1"
}
bar='/tmp/dir'
cmd mkdir -p -- "$bar"
cmd ls -l -- "$bar"
Output.
mkdir -p '/tmp/dir'
ls -l '/tmp/dir'
I understood shellcheck intends to lint shell scripts and incorrect warnings can only make people lose confidence in its ability to do just that.
ShellCheck will not warn about unquoted variables used as command names, e.g.
with the rationale that you’re far more likely to have a variable with a command name and flags, than to have a command name with embedded spaces.
However, it fails to realize that $foo
is used in the same context when passed as the first non-flag parameter to command
. It probably should.
I have come up with a work around to silence this warning. Here is an updated example.
#!/bin/sh
set -eu
foo='ls -l'
bar='/tmp/dir'
mkdir -p -- "$bar"
eval "set -- $foo"
command -p "$@" "$bar"
Note, I had difficulty applying the above to my more elaborate example until I added this to the top the script.
PATH="$(command -p getconf PATH):$PATH"
For some reason ksh did not like this with command -p
, but was fine with just command
.
Вам нужно использовать "$(somecmd "$file")"
.
Без кавычек путь с пробелом будет разделен в аргументе somecmd
, и он будет нацелен на неправильный файл. Так что вам нужны цитаты изнутри.
Любые пробелы в выходных данных somecmd
также будут вызывать расщепление, поэтому вам нужны кавычки вне всей подстановки команд.
Кавычки внутри подстановки команд не влияют на кавычки вне ее. Собственное справочное руководство Bash не слишком ясно об этом, но BashGuide прямо упоминает об этом . Текст в POSIX также требует, так как «любой действительный сценарий оболочки» допускается внутри $(...)
:
В
$(command)
форме все символы, следующие за открывающей скобкой и совпадающей с закрывающей скобкой, составляют команду. Любой допустимый сценарий оболочки может быть использован для команды, кроме сценария, состоящего исключительно из перенаправлений, который приводит к неопределенным результатам.
Пример:
$ file="./space here/foo"
а. Без кавычек, dirname
обрабатывает как ./space
и here/foo
:
$ printf "<%s>n" $(dirname $file)
<.>
<here>
б. Кавычки внутри, dirname
процессы ./space here/foo
, дачи ./space here
, которые делятся на две части:
$ printf "<%s>n" $(dirname "$file")
<./space>
<here>
с. Кавычки снаружи, dirname
обрабатывают оба ./space
и here/foo
выводят на отдельных строках, но теперь две строки образуют один аргумент :
$ printf "<%s>n" "$(dirname $file)"
<.
here>
д. Цитаты как внутри, так и снаружи, это дает правильный ответ:
$ printf "<%s>n" "$(dirname "$file")"
<./space here>
(возможно, было бы проще, если бы dirname
только обработал первый аргумент, но это не показало бы разницу между случаями a и c.)
Обратите внимание, что с dirname
(и, возможно, с другими) вам также нужно добавить --
, чтобы предотвратить использование имени файла в качестве опции на случай, если оно начинается с тире, поэтому используйте "$(dirname -- "$file")"
.
ShellCheck — утилита для отладки ваших shell скриптов. Он позволит улучшить ваши написанные bash скрипты.
Установка ShellCheck в Debian/Ubuntu Linux
Выполняем команду:
# apt install shellcheck -y
Очень простая установка.
Установка ShellCheck в CentOS/RHEL/Fedora/Oracle Linux
Для начала, подключаем EPEL репозиторий, описание тут — включить EPEL репозиторий на CentOS
После чего, выполняем:
# yum install ShellCheck -y
PS: При использовании, Fedora — можно использовать:
# dnf install ShellCheck -y
Установка ShellCheck в Arch Linux
Выполняем команду:
# pacman -S shellcheck
Установка ShellCheck в Gentoo Linux
Выполняем команду:
# emerge --ask shellcheck
Установка ShellCheck в OpenSUSE Linux
Выполняем команду:
# zypper in ShellCheck
Установка ShellCheck в macOS Unix
Подключаем HOMEBREW, если нужно помощь, вот запить о том как можно выполнить установку brew — Установка homebrew на Mac OS X
Далее, стоит выполнить:
$ brew install shellcheck
Можно переходить к использовании.
Использование ShellCheck в Unix/Linux
Приведу наглядный пример использования данной утилиты на примере моего написанного скрипта. Для начала, я его возьму из гита:
# cd /usr/local/src && wget raw.githubusercontent.com/SebastianUA/redis/master/flush-cache.sh
И после чего, запускаем тестирование:
# shellcheck flush-cache.sh
Получаем вывод:
In flush-cache.sh line 1: #!/usr/bin/env bash -x ^-- SC2096: On most OS, shebangs can only specify a single parameter. In flush-cache.sh line 18: SETCOLOR_NUMBERS="echo -en \033[0;34m" #BLUE ^-- SC2034: SETCOLOR_NUMBERS appears unused. Verify it or export it. In flush-cache.sh line 21: if [ "`whoami`" = "root" ]; then ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 56: echo" `whoami 2> /dev/null` doesn't have permissions. Please use ROOT user for it!"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 64: echo "**************************************************************" >> $FlushCacheReport ^-- SC2129: Consider using { cmd1; cmd2; } >> file instead of individual redirects. In flush-cache.sh line 65: echo "HOSTNAME: `hostname`" >> $FlushCacheReport ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 78: echo "expect has been INSTALLED on this server: `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 82: echo "expect INSTALLED on this server: `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 86: if [ -z "`rpm -qa | grep mailx`" ]; then ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 89: echo "service of mail has been installed on `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 93: echo "mailx INSTALLED on this server: `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 102: echo "expect has been INSTALLED on this server: `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 106: echo "expect INSTALLED on this server: `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 110: if [ -z "`which mailx`" ]; then ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 113: echo "service of mail has been installed on `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 117: echo "mailx INSTALLED on this server: `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 123: echo 'OS=' $OS 'VER=' $VER ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 130: if [ $? -eq 0 ]; then ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?. In flush-cache.sh line 132: echo -n "$(tput hpa $(tput cols))$(tput cub 6)[OK]" ^-- SC2046: Quote this to prevent word splitting. In flush-cache.sh line 137: echo -n "$(tput hpa $(tput cols))$(tput cub 6)[fail]" ^-- SC2046: Quote this to prevent word splitting. In flush-cache.sh line 144: for Roots in `echo $RootF|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 156: for Iconfig in `ls -al /etc/nginx/conf.d/*.conf | grep "^-"| grep -vE "(default|geo|example)"|awk '{print $9}'|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2010: Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames. In flush-cache.sh line 157: RootF=$(cat $Iconfig 2> /dev/null| grep root|cut -d ";" -f1 | awk '{print $2}'|grep -vE "(SCRIPT_FILENAME|fastcgi_param|fastcgi_script_name|log|-f)"|uniq| grep -vE "(blog|wp)") ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 158: SITE=$(cat $Iconfig 2> /dev/null| grep "server_name"|awk '{print $2}'|cut -d ";" -f1) ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 159: echo $SITE ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 165: for Iconfig in `ls -alR /etc/httpd/conf.d/*.conf | grep "^-"| grep -vE "(default|geo|example)"|awk '{print $9}'|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2010: Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames. In flush-cache.sh line 166: RootF=$(cat $Iconfig 2> /dev/null| grep DocumentRoot| cut -d '"' -f2|uniq| grep -v "blog") ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 167: SITE=$(cat $Iconfig 2> /dev/null| grep -E "ServerName"|awk '{print $2}') ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 172: echo "Please check which web-server installed on `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 179: for Iconfig in `ls -al /etc/nginx/conf.d/*.conf | grep "^-"| grep -vE "(default|geo|example)"|awk '{print $9}'|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2010: Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames. In flush-cache.sh line 180: RootF=$(cat $Iconfig 2> /dev/null| grep root|cut -d ";" -f1 | awk '{print $2}'|grep -vE "(SCRIPT_FILENAME|fastcgi_param|fastcgi_script_name|log|-f)"|uniq| grep -vE "(blog|wp)") ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 181: SITE=$(cat $Iconfig 2> /dev/null| grep "server_name"|awk '{print $2}'|cut -d ";" -f1) ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 182: echo $SITE ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 189: for Iconfig in `ls -alR /etc/apache2/sites-enabled/*.conf| awk '{print $9}'|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2012: Use find instead of ls to better handle non-alphanumeric filenames. In flush-cache.sh line 190: RootF=$(cat $Iconfig 2> /dev/null| grep DocumentRoot| cut -d '"' -f2|uniq| grep -v "blog") ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 191: SITE=$(cat $Iconfig 2> /dev/null| grep -E "ServerName"|awk '{print $2}') ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 196: echo "Please check which web-server2 installed on `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 209: CacheRedisIP=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13| grep "<server>"|uniq|cut -d ">" -f2 | cut -d "<" -f1) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 211: CacheRedisIP=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13| grep "<server>"| uniq|cut -d "[" -f3| cut -d "]" -f1) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 215: CacheRedisSock=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13| grep "<server>"|uniq| cut -d ">" -f2|cut -d "<" -f1| cut -d "." -f2) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 217: CacheRedisSock=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13| grep "<server>"| uniq| cut -d ">" -f2|cut -d "<" -f1| cut -d "." -f2) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 221: CacheRedisPorts=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13| cut -d '>' -f2| grep port | cut -d '<' -f1|uniq) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 223: CacheRedisPorts=$(cat `echo $LocalXML 2> /dev/null` |grep Cache_Backend_Redis -A13 | grep port | cut -d "[" -f3| cut -d "]" -f1| grep -Ev "gzip"|uniq) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 232: CacheRedisDB=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13 | grep database | cut -d ">" -f2 |cut -d "<" -f1|uniq) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 237: echo "CacheRedisIPs: `echo $CacheRedisIP 2> /dev/null`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 241: echo "redis-cli -s `echo $CacheRedisIP` flushall"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 242: echo 'flushall' | redis-cli -s `echo $CacheRedisIP`; ^-- SC2046: Quote this to prevent word splitting. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 245: for ICacheRedisIP in `echo $CacheRedisIP|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 246: for ICacheRedisPorts in `echo $CacheRedisPorts|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 247: echo "Cache-redis-ports: `echo $CacheRedisPorts 2> /dev/null`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 251: if [ -n "`whereis redis-cli| awk '{print $2}'`" ]; then ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 252: R_flush=$(redis-cli -h `echo $ICacheRedisIP` -p `echo $ICacheRedisPorts` flushall) ^-- SC2034: R_flush appears unused. Verify it or export it. ^-- SC2046: Quote this to prevent word splitting. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2046: Quote this to prevent word splitting. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 254: echo "redis-cli -h `echo $ICacheRedisIP` -p `echo $ICacheRedisPorts` flushall"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 260: echo $ICacheRedisIP '+' $ICacheRedisPorts ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 272: echo "CacheRedisDB = `echo $CacheRedisDB 2> /dev/null`" ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 275: Server_port="SERVER::::> `echo $ICacheRedisIP` PORT::::> `echo $ICacheRedisPorts`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 277: echo "`echo $Server_port`"; ^-- SC2005: Useless echo? Instead of 'echo $(cmd)', just use 'cmd'. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 278: for ICacheRedisDB in `echo $CacheRedisDB|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 281: echo "`echo $Server_port` DataBase::::> `echo $ICacheRedisDB`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 283: CacheRedisDBAuth=$(cat `echo $LocalXML` 2> /dev/null| grep Cache_Backend_Redis -A13 | grep password | cut -d ">" -f2 |cut -d "<" -f1|uniq) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 304: for ICacheRedisDBAuth in `echo $CacheRedisDBAuth|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 340: echo "Local Cache on server `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 344: `rm -rf echo $Cache_Dir` ^-- SC2092: Remove backticks to avoid executing output. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 359: MemcachedServer=$(cat `echo $LocalXML` 2> /dev/null | grep -Ev ^$| grep '<memcached>' -A7| grep -E 'host|CDATA|port' | grep -v "ersistent"| grep host| cut -d "[" -f3| cut -d "]" -f1|uniq) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 360: MemcachedPort=$(cat `echo $LocalXML` 2> /dev/null | grep -Ev ^$| grep '<memcached>' -A7| grep -E 'host|CDATA|port' | grep -v "ersistent"| grep port| cut -d "[" -f3| cut -d "]" -f1|uniq) ^-- SC2046: Quote this to prevent word splitting. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 366: echo "Memcached Server => `echo $MemcachedServer`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 367: echo "Memcached Port => `echo $MemcachedPort`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 369: `which expect | grep -E expect` <<EOF ^-- SC2092: Remove backticks to avoid executing output. ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 378: echo "memcached has been flushed on server `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 382: echo "Din't find memcached on server `hostname`"; ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 391: for IRootFolder in `cat $RootFolder|xargs -I{} -n1 echo {}` ; do ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. In flush-cache.sh line 393: echo " ~~~~~~ `echo $SITE` ~~~~~~ "; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 402: echo "Root-XML with '/' : `echo $LocalXML| grep -vE "DocumentRoot"`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 412: echo "Root-XML: `echo $LocalXML`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. ^-- SC2086: Double quote to prevent globbing and word splitting. In flush-cache.sh line 422: mail -s " HOSTNAME is `hostname`" $List_of_emails < $FlushCacheReport ^-- SC2006: Use $(..) instead of legacy `..`. In flush-cache.sh line 423: if [ $? -eq 0 ]; then ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?. In flush-cache.sh line 425: echo "LOG_FILE= $FlushCacheReport has been sent to `echo $List_of_emails`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. In flush-cache.sh line 429: echo "The email hasn't been sent to `echo $List_of_emails`"; ^-- SC2006: Use $(..) instead of legacy `..`. ^-- SC2116: Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'.
Утилита поможет отладить ваши баш-скрипты помогая советами (как видно с вывода моего скрипта).
Интеграция ShellCheck в текстовый редактор
ShellCheck позоляет произвести интеграцию с некоторыми текстовыми редакторами (vim, emacs). Он может проверять ваш код прямо в текстовом редакторе. Для этого стоит установить:
- pearofducks/ansible-vim
- neomake/neomake
Давайте установим эти плагины для vim, открываем:
# vim ~/.vimrc
Прописываем:
call plug#begin('~/.vim/autoload') Plug 'pearofducks/ansible-vim' Plug 'pearofducks/ansible-vim', { 'do': './UltiSnips/generate.py' } Plug 'neomake/neomake' call plug#end()
Чтобы установить плагины, откройте редактор вим, и в нем введите:
:PlugInstall
Теперь, открываем любой плагин и вводим:
:Neomake
Получаем подсказки.
На этом у меня все, статья «Установка ShellCheck в Unix/Linux » подошла к завершению.
Bash-скрипты — эффективное решение для автоматизации рутинных задач, но не всегда самое простое. Объемные сценарии характеризуются низкой производительностью и сложны для чтения. В этой статье мы рассмотрим, как оптимизировать работу, упростить с помощью утилит sed и awk и не совершать очевидных ошибок в написании скриптов.
Настройка выполнения скриптов
Управление процессами в Linux увеличивает коэффициент полезного использования ЦП, памяти, устройств ввода-вывода и других составляющих системы. Рассмотрим, как применить эти принципы и команды при запуске bash-скриптов в работу.
Скорость работы сценария зависит от количества операций и их «энергозатратности» для ресурсов системы. Проблема в том, что во время его выполнения системный администратор не может использовать консоль. Как решить эту задачу?
-
Использовать терминальные мультиплексоры: screen или более продвинутый tmux, о которых слышали даже «новички» в Unix-среде. Программы позволяют разделить терминал, создав несколько окон и поддерживая несколько сессий одновременно.
-
Перемещать процесс выполнения скрипта в фоновый режим. Если скрипт еще не запущен, то добавьте амперсанд & в конце команды
$ ./slurmscript &
Когда скрипт уже выполняется, приостановите его сочетанием клавиш Ctrl+Z и используйте команду $ bg для продолжения его работы, но уже в фоновом режиме. А чтобы вернуть скрипт на «передний план», введите после Ctrl+Z $ fg. Без указания дополнительных параметров опция будет применяться к текущему выполняемому заданию. Список всех задач оболочки отображается командой jobs:
$ jobs
[1] Stopped ./slurmscript
[2] Running ./slurmscript > slurmfile &
$ fg 1 #перевод в активный режим задачи №1 в списке
Несмотря на то, что сценарий работает в фоновом режиме, выходные данные выдаются на экран терминала. А выполнение скрипта остановится при отключении от сервера. Чтобы этого избежать:
-
Команда nohup нарушит прямую связь между выполняемым скриптом и терминалом. Сценарий не остановится в момент выхода из сессии, а вывод данных запишется в файл nohup.log.
$ nohup ./slurmscript &
Если скрипт не выводит никаких данных, файл останется пустым. Перенаправьте вывод в /dev/null, чтобы файл не был создан автоматически.
$ nohup ./slurmscript >/dev/null &
-
Команда disown -h вводится после запуска или перевода скрипта в фоновый режим. Выполнение сценария будет продолжаться и после закрытия сессии терминала.
$ ./slurmscript & +
[1] 4545
$jobs
[1] + Running ./slurmscript &
[2] - Running ping slurm.io & >/dev/null
$disown -h
$jobs
[1] + Running ping slurm.io & >/dev/null
Disown -h можно использовать без дополнительных параметров, когда она применяется для текущего процесса. В других случаях, необходимо ввести номер строки в списке или pid (идентификатор) процесса. В результате выполняемый скрипт исчез из списка заданий оболочки и не получит сигнала о выходе из терминала. Чтобы убедиться, что процесс продолжается, используйте команды ps или top.
-
Направить stdout скрипта в конкретный файл можно с помощью такой команды
$ ./slurmscript > slurmfile &
Не менее полезные инструменты для управления скриптами — at, cron и anacron. Утилиты автоматизируют любые процессы, в том числе запуск сценариев:
-
At — команда однократно задающая дату и время начала процесса.
# время дата
$at -f /home/slurm/slurm1 10:30 01092022
Ключ -f необходимо использовать для указания файла утилите, вместо конкретного процесса. Команда at распознает разнообразные форматы указания даты: $at 01:00 PM (час дня), $at now+10 minutes (через 10 минут), $at 09.00 AM next month (ровно через месяц в 10 утра), $at tomorrow (через 24 часа). Для завершения работы с установкой времени, нажмите Ctrl+D, просмотреть заданные параметры утилиты — $atq, а удалить — $atrm c указанием номера в списке.
-
Cron — позволяет многократно выполнять процессы по заданному расписанию. Установить дату и время для скриптов можно в конфигурационном файле crontab. Для работы с файлом используются три основные команды: $crontab -l (просмотреть все задачи cron), $crontab -r (удалить все записи) и $crontab -e (внести изменения в задачи для cron). Редактирование планировщика строится на заполнении данных пяти полей временного интервала и поля, указывающего полный путь к скрипту:
#мин чаcы день месяц день недели скрипт
* * * * * /home/slurm/slurm1
Допустимые значения для категории минут — от нуля до 59, часов — от 0 до 23, дней месяца — от 1 до 31, месяцев — от 1 до 12 и дней недели — от 0 до 6 или от 1 до 7 (в некоторых версиях воскресенье обозначается нулем, а в других — семеркой). Если ни одно поле не будет заполнено, то планировщик станет запускать скрипт каждую минуту, каждого часа и т.д.
0 8,20 * */2 1-5 /home/slurm/slurm1
-
Согласно настройкам скрипт slurm1 будет запускаться в фоновом режиме в восемь утра и восемь вечера (8, 20) ноль минут (0), независимо от числа (*), но только с пн по пт (1-5) и каждые два месяца (*/2).
-
Anacron — отличается от Cron тем, что изменения в конфигурационный файл может вносить только root, учитываются невыполненные задачи во время отключения компьютера, вместо точного времени можно задать только интервал. Для настройки периодичности заданий используются те же команды, что и для Cron.
Sed и awk
Инструменты обработки текста значительно расширяют возможности оболочки bash. Но с командами sed и awk можно не только редактировать вывод и файлы, включая сами скрипты. Утилиты служат наиболее эффективным решением некоторых задач автоматизации процессов.
Sed — потоковый редактор файлов, позволяющий сэкономить время на выполнении простых функций: удаление, замена, вставка текста. Как это работает? Утилите передается информация в виде набора условий. Она по очереди «читает» строки в файле или файлах и применяет заданные правила: sed ‘[область применения] [опция] / [шаблон для изменения] / [новый шаблон] / [w обновленный файл]’ [исходный файл].
Например: заменить в третьей строке файла Slurm выражение «New course» на «Linux Mega» и сохранить изменения в копии исходного файла slurm1
$ sed '3s/new course/linux mega/w slurm1' slurm
Количество задаваемых параметров может варьироваться. Если правило не указано, то действуют настройки по умолчанию. Без указания исходного и нового файлов данные берутся и выводятся в терминал.
Область применения заданных правил можно обозначить конкретной строкой, как в примере, диапазоном строк (2,3 — вторая и третья; 4,$ — с четвертой строки и до конца файла) или как полный текст (ключ g после [новый шаблон] —
$ sed 's/new course/linux mega/g' slurm)
Если не конкретизировать область, то по умолчанию операция производится над первым соответствующим выражением в каждой строке.
Опции, которые могут пригодиться для создания скрипта: s — замена шаблона выражений, y — замена шаблона символов, d — удаление строк, i — вставка перед указанной строкой, а — вставка текста после указанной строки. Команда может содержать несколько правил внесения изменений, перечисленных через точку с запятой. Чтобы она сработала необходимо добавить ключ -e перед «областью применения»:
$sed -e '/^#/d; /^$/d' slurm1
Sed читает первую строку файла slurm1 в поисках совпадений с одним или всеми заданными в слешах (//) шаблонами. Набор символов «^#» обозначает строки, начинающиеся (^) со знака #, то есть комментарии. А символы «^$» — пустые строки, в которых нет никаких знаков от начала строки (^) до ее окончания ($). Если первая строка подходит под заданные параметры, sed анализирует, входит ли строка в область применения и выполняет необходимое действие, то есть удаляет ее, а затем обращается к следующей строке.
Эту команду можно ввести разными способами: перечислив шаблоны через запятую перед «d» или отделяя каждую опцию переходом на новую строку вместо «;». Но наиболее часто используемый вариант написания в скриптах:
$sed -e '/^#/d' -e '/^$/d' slurm1
Команда может содержать несколько шаблонов, операций или длинную строку регулярных выражений. Потому повторное введение ключа -e упрощает чтение и понимание файла сценария администратором.
Awk — язык программирования, синтаксис которого напоминает языки C и Perl. Хотя awk работает по тому же «построчному» принципу, но значительно превосходит sed по функциональным возможностям. При написании bash-скриптов инструмент удобно использовать для работы со структурированными данными, так как awk воспринимает поля (область текста, отделенную пробелами или табуляцией), переменные, арифметические функции и др.
Данные по умолчанию принимаются утилитой и выводятся после применения новых условий в терминал. Но можно обозначить в команде необходимый файл для обработки, когда это требуется.
Если выстроить параметры по аналогии с примером для утилиты sed, то команда будет выглядеть так: $awk [опция] ‘{[функция] [шаблон для изменения]}’ [исходный файл]. Подобная схема получилась условной, так как в [опциях] и [функциях] могут использоваться дополнительные ключи, переменные, циклы, операторы обработки текста, например, сравнения и совпадения. Ниже отрывок из скрипта для выявления пользователей, расходующих большой объем дискового пространства:
for name in $(cut -d: -f1,3 /etc/passwd |
awk -F: '$2 > 99 {print $1}')
do
/bin/echo -en "User $name exceeds disk quota.n"
done
Где -F: — опция для разделения текста (двоеточие — пример знака, по которому строки делятся на поля), $2 и $1 — обозначение столбца данных, print — функция для вывода измененных данных, а /etc/password — имя исходного файла. То есть утилита просматривает данные второго столбца в файле, уже отредактированном командой cut, сравнивает значения с числом 99 и выводит данные первого столбца, то есть имена пользователей из файла /etc/passwd.
Бесполезный для сценариев, но более ясный пример с использованием нескольких команд, перечисленных через точку с запятой:
$ echo "Moscow is the capital of GB" | awk '{$1="London"; print $0}'
London is the capital of GB
Сначала утилита заменяет текст первого поля на значение в кавычках «London», а затем выводит всю строку ($0) измененного комментария.
Awk использует множество опций. Помимо самой распространенной -F: могут потребоваться -v var=value — задать переменную, -o — вывести обработанный текст в файл, -f — указать файл сценария для выполнения.
Процесс работы awk делиться на 3 этапа: до, обработка текста и после нее. Используя опцию BEGIN, можно задать переменные или вставить текст до отображения данных:
$ awk 'BEGIN {FIELDWIDTHS="1 3 5"} {print $1, $2, $3}' newfile
Прежде, чем вывести числовые значения из файла, утилита структурирует их согласно заданной опции FIELDWIDTHS по количеству знаков. Тогда в первом столбце будет показана 1 цифра из строки, через пробел еще 3 цифры и потом 5 оставшихся. А с помощью опции END получится вывести результат выполненного действия:
$ awk 'END { print "Course consists of", NR, "modules"}' file.txt
Например, в основном процессе обработки были внесены изменения количества строк в файле, а после ее завершения необходимо вывести это количество (опция NR) в удобочитаемом варианте.
Кроме собственных опций и ключей утилиты sed и awk поддерживают использование регулярных выражений. Так можно задать команде awk вывод строк, содержащих цифры, специальным набором символов [[:digit:]].
awk '/[[:digit:]]/{print $0}'
И более сложный пример для утилиты sed — замена первых пяти вхождений конструкций «цифра-цифра-точка» из каждой строки файла:
sed -e 's/([[:digit:]][[:digit:]].){5}//g'
-e 's/^/ /'
Такое нагромождение знаков вызвано необходимостью выделить (/ или ) метасимволы ({, [, ^ и т.д.), чтобы оболочка верно их прочитала.
Bash-скрипт Unrm
На профессиональных форумах часто ведутся споры о выборе правильного инструмента для решения задачи. Некоторые специалисты считают, что bash-скрипты должны ограничиваться 10-15 строками, другие — одним циклом или одной функцией. Тогда как, третьи пишут сложные многоуровневые скрипты. Вот один из примеров реально работающих сценариев для ознакомления, часть действий которого проще и эффективнее организовать функциями утилит sed и awk.
Bash-скрипт для восстановления резервных копий.
#!/bin/bash
# Unrm — находит в архиве резервных копий удаленных файлов тот, который запрашивается пользователем. В случае наличия нескольких резервных копий одного файла, выводит их списком с сортировкой по времени.
archivedir="$HOME/.deleted-files"
realrm="$(which rm)"
move="$(which mv)"
dest=$(pwd)
if [ ! -d $archivedir ] ; then
echo "$0: No deleted files directory: nothing to unrm" >&2
exit 1
fi
cd $archivedir
if [ $# -eq 0 ] ; then
echo "Contents of your deleted files archive (sorted by date):"
# sed используется с регулярным выражением и удаляет заданные шаблоны символов из вывода каждой строки команды ls. Такая информация не имеет ценности и загромождает вывод списка
ls -FC | sed -e 's/([[:digit:]][[:digit:]].){5}//g'
-e 's/^/ /'
exit 0
fi
matches="$(ls -d *"$1" 2> /dev/null | wc -l)"
if [ $matches -eq 0 ] ; then
echo "No match for "$1" in the deleted file archive." >&2
exit 1
fi
if [ $matches -gt 1 ] ; then
echo "More than one file or directory match in the archive:"
index=1
for name in $(ls -td *"$1")
do
datetime="$(echo $name | cut -c1-14|
# awk заменяет префикс имени файла на дату удаления исходного файла
awk -F. '{ print $5"/"$4" at "$3":"$2":"$1 }')"
filename="$(echo $name | cut -c16-)"
if [ -d $name ] ; then
filecount="$(ls $name | wc -l | sed 's/[^[:digit:]]//g')"
echo " $index) $filename (contents = ${filecount} items,"
" deleted = $datetime)"
else
size="$(ls -sdk1 $name | awk '{print $1}')"
echo " $index) $filename (size = ${size}Kb, deleted = $datetime)"
fi
index=$(( $index + 1))
done
echo ""
/bin/echo -n "Which version of $1 should I restore ('0' to quit)? [1] : "
read desired
if [ ! -z "$(echo $desired | sed 's/[[:digit:]]//g')" ] ; then
echo "$0: Restore canceled by user: invalid input." >&2
exit 1
fi
if [ ${desired:=1} -ge $index ] ; then
echo "$0: Restore canceled by user: index value too big." >&2
exit 1
fi
if [ $desired -lt 1 ] ; then
echo "$0: Restore canceled by user." >&2
exit 1
fi
# sed служит для извлечения из stdout ключами -n и p строки, в которой указана искомая копия файла
restore="$(ls -td1 *"$1" | sed -n "${desired}p")"
if [ -e "$dest/$1" ] ; then
echo ""$1" already exists in this directory. Cannot overwrite." >&2
exit 1
fi
/bin/echo -n "Restoring file "$1" ..."
$move "$restore" "$dest/$1"
echo "done."
/bin/echo -n "Delete the additional copies of this file? [y] "
read answer
if [ ${answer:=y} = "y" ] ; then
$realrm -rf *"$1"
echo "Deleted."
else
echo "Additional copies retained."
fi
else
if [ -e "$dest/$1" ] ; then
echo ""$1" already exists in this directory. Cannot overwrite." >&2
exit 1
fi
restore="$(ls -d *"$1")"
/bin/echo -n "Restoring file "$1" ... "
$move "$restore" "$dest/$1"
echo "Done."
fi
exit 0
Главный совет при создании bash-скриптов — подумать прежде, чем писать. Чтобы сценарий выполнялся корректно, был удобен для дальнейшего использования или масштабирования, необходимо определиться с его структурой и логикой построения до начала работы.
Как надо:
-
научитесь пользоваться константами при вводе одинаковых значений или утилитой sed для удобства внесения изменений в скрипт в будущем;
-
дробите сложные сценарии на небольшие части и используйте функции;
-
задавайте рациональные имена переменным и функциям;
-
вносите комментарии, чтобы смысл команды не забылся, а сценарий мог обслуживать не только автор;
-
определитесь с регистром, типами скобок, сделайте стиль единообразным;
-
не используйте ресурсозатратные операции внутри циклов, например, find;
-
утилиты могут эмулировать функции встроенной команды bash или другой утилиты, то есть выводить один и тот же результат. В этом случае отдайте предпочтение встроенным командам или команде, содержащей меньше процессов.
И как не надо:
-
в именах переменных не должно быть служебных символов bash и они не должны совпадать с именами функций или специальными словами:
"var-3=19" или "time=19"
-
команда, для запуска которой пользователю не хватает прав доступа, не будет работать и в запущенном им сценарии;
-
перенос файла со сценарием, сохраненного в Windows, требует решения проблемы деления строк. В MS-DOS строка завершается двумя символами:
#!/bin/bashrn
-
а в Linux — одним:
#!/bin/bashn
Удалить лишнюю -r можно с помощью редактора vim или утилит dos2unix и unix2dos.
-
дополнительные пробелы, свойственные языку С foo = bar
-
символы && и || не эмулируют команду if… then… else… fi в большинстве случаев;
-
искать опечатки в переменных через команду set -u (лучше использовать sell check).
Краткий обзор полезных функций для совершенствования сценариев — завершен.
Для тех, кто до конца не разобрался или хочет узнать больше полезных советов
28 июля 2022 стартует практический курс Southbridge + Слёрм «Администрирование Linux. Mega». Опыт автора программы инженера Southbridge Платона Платонова не ограничивается написанием bash-скриптов, а подкрепляется ни единым кейсом «best of the best» practice.
Вас ждут: 5 недель, 9 блоков, 12 часов теории, 48 часов практики на стендах, 80 lvl владения OC по окончанию. Углубьте свои знания работы с Linux за месяц.
Смотреть программу и занять местечко в потоке 28 июля: https://slurm.club/3yEGiAf
shellcheck doesn’t always give the best advice. Here are some examples where you should ignore what shellcheck is saying.
Using printf with dynamic formats:
^— SC2059: Don’t use variables in the printf format string. Use printf «..%s..» «$foo».
It is not always a mistake to use a variable in the format argument of printf. This should be ignored if you are using a dynamic format string.
Using parameter expansion inside the argument space of the :
built-in:
: ${TERMINAL_STDOUT_PASSTHRU:=3}
^— SC2086: Double quote to prevent globbing and word splitting.
This should be ignored because shellcheck isn’t smart enough to understand that the expansion of the variable is ignored because it appears in the argument space of the :
built-in. You do not need to add double-quotes as it suggests.
This same error pops up when you use a variable argument to the return
builtin. return only accepts an integer argument and thus it would be of little use to quote the variable when you know it will only ever be a number. The warning about word splitting does not apply in the below situation:
[ «$__name» ] || return $FAILURE
^— SC2086: Double quote to prevent globbing and word splitting.
Rest assured, any instance of the following will suffer from the same bad advice:
^— SC2086: Double quote to prevent globbing and word splitting.
Using sub-shells in the argument space of the export
built-in:
export UNAME_S=»$( uname -s )» # Operating System (i.e. FreeBSD)
^— SC2155: Declare and assign separately to avoid masking return values.
What shellcheck refers to as «return value» is actually the «exit status» of the sub-shell where uname -s
appears. If you don’t care about the error, it should not matter that you put the sub-shell in the argument space of another command.
Using continue inside a loop when inside a function
In many languages, quotes are primarily used to denote string literals. In the shell paradigm many constructs are interpreted as strings by default, so quotes play other important roles. Quotes demarcate the arguments of a command into units called «words», as well as modify the evaluation of their contents in numerous context-dependent ways. It is critical to understand how quoting affects the interpretation of code in a given context. It’s something no one should avoid learning. Improper quoting is among the most common shell programming errors. Do not guess about quotes!
Contents
- I’m Too Lazy to Read, Just Tell Me What to Do
- Types of quoting
-
Effects of Quoting
- Preserve unescaped metacharacters
- Prevent field splitting and ignore glob pattern characters
- Expand argument lists
- When Should You Quote?
-
Examples
- Patterns
- Here Documents
- See also
I’m Too Lazy to Read, Just Tell Me What to Do
cp $file $destination # WRONG cp -- "$file" "$destination" # Right
When in doubt, double-quote every expansion in your shell commands.
Types of quoting
There are three standard types of quotes (or four if you count backslash escaping), and two nonstandard Bash extensions.
-
Single quotes: ‘…’ removes the special meaning of every character between the quotes. Everything inside single quotes becomes a literal string. The only character that you can’t safely enclose in single quotes is a single quote.
-
Double quotes: «…» prevents some substitutions but allows others. Every substitution that begins with a dollar sign $ is performed, as is the legacy `…` (backtick) command substitution. Backslash escaping is also performed. No word splitting or filename expansion is performed.
-
Backticks: `…` is the legacy command substitution syntax; deprecated in favor of $(…) but still permitted for historical reasons. See FAQ 082 for details.
-
Backslash: Putting in front of a metacharacter removes its special meaning. This works inside double quotes, or in the absence of quotes. It does not work inside single quotes.
-
$’…’ : Contents are a single word with interpretation of backslash escape sequences such as n for newline, t for tab, and xnn for bytes specified in hexadecimal. These may be used to specify a text representation of arbitrary data. No current implementation supports a context where these are not interpreted as NUL-terminated C strings.
-
$»…» : This is a Bash extension. It is used for localization support and will not be covered on this page.
You may concatenate the various types of quoting if you need to. For example, if you have one section of a string that has lots of special characters that you’d like to single-quote, and another section with a parameter expansion in it which must be double-quoted, you may mix them:
$ foo=bar $ printf '%sn' '!%$*&'"$foo" !%$*&bar
Any number of quoted substrings, of any style, may be concatenated in this manner. The result (after appropriate expansions in the double-quoted sections) is a single word.
Effects of Quoting
Preserve unescaped metacharacters
A shell command is parsed by the shell into words, using whitespace other metacharacters. The first function of quoting is to permit words to contain these metacharacters.
echo '&'
Without quotes, the & would put the echo command into the background. With quotes, the & is simply made into a word, and passed as an argument to the echo command instead.
The quotes are not actually passed along to the command. They are removed by the shell (this process is cleverly called «quote removal»). In the example above, the echo command sees only the &, not the quotes.
Prevent field splitting and ignore glob pattern characters
The second purpose of quoting is to prevent word splitting and globbing. The result of a double-quoted substitution does not undergo any further processing (whereas the result of an unquoted substitution does).
cp -- "$filename" "$destination"
In this example, the double quotes protect the value of each parameter (variable) from undergoing word splitting or globbing should it happen to contain whitespace or wildcard characters (* or ? or […]). Without the quotes, a filename like hot stuff.mp3 would be split into two words, and each word would be passed to the cp command as a separate argument. Or, a filename that contains * with whitespace around it would produce one word for every file in the current directory. That is not what we want.
With the quotes, every character in the value of the filename parameter is treated literally, and the whole value becomes the second argument to the cp command.
When in doubt, always double-quote your parameter expansions.
Expand argument lists
Double-quoting $@ or ${array[@]} has a special meaning. «$@» expands to a list of words, with each positional parameter’s value being one word. Likewise, «${array[@]}» expands to a list of words, one per array element. When dealing with the positional parameters or with the contents of an array as a list of words, always use the double-quoted syntax.
Double-quoting $* or ${array[*]} results in one word which is the concatenation of all the positional parameters (or array elements) with the first character of IFS between them. This is similar to the join function in some other languages, although the fact that you can only have a single join character can sometimes be a crippling limitation.
When Should You Quote?
The basic rule of thumb is that you should double-quote every expansion. This prevents unwanted word splitting and globbing. When in doubt, quote it.
There are a few cases where double quotes may be safely omitted:
-
On the right hand side of a simple assignment. You may write foo=$bar without quotes. This is POSIX compliant.
-
The word following a case keyword. You may write case $foo in … safely. This is POSIX compliant.
-
Inside a [[ command, except on the right hand side of an = or == operator. [[ already suppresses word splitting and globbing, so you can write things like [[ -z $string ]] safely if you wish. However, be warned that [[ foo = $bar ]] and [[ foo = «$bar» ]] act differently (see patterns below). The [[ keyword is a Bash extension.
Use single quotes when protecting complex strings, especially ones that contain shell syntax which you don’t want evaluated.
Examples
Here are some assorted examples, to show how things should be done. Some of these examples use bash/ksh syntax that won’t work in strict POSIX shells.
Proper iteration over the positional parameters using a quoted «$@». Never use an unquoted $@ or $*.
for file in "$@"; do ... done
As above, except with an array:
for element in "${array[@]}"; do ...
Proper iteration over array indexes:
# bash 3.0 and higher for index in "${!array[@]}"; do ...
All of the usual expansions apply to text within the parentheses of a compound array assignment including word splitting and pathname expansion, and must be quoted and escaped in the same way as though they were to be passed as arguments to a command:
# bash or ksh93 find_opts=( ( -iname '*.jpg' -o -iname '*.gif' -o -iname '*.png' ) ) find . "${find_opts[@]}" -print
There are generally three alternatives for encoding a literal string containing quotes. Which works best depends on the context. First, a single quoted string that can contain anything other than single-quotes. In this case, an escaped, unquoted single-quote is concatenated with the argument between two single quoted strings. Second, a double-quoted string with all expansions and double-quotes within escaped. Third, a less portable equivalent using $’…’:
printf '%sn' 'Don'''t walk!' printf '%sn' "Don't walk!" printf '%sn' $'Don't talk!'
$(…)-style command substitutions are unique in that the quoting of their contents is completely independent to their surroundings. This means you don’t have to worry about nested quote escaping problems:
printf '%sn' "The matching line is: $(grep foo "$filename")" # Note that the quotes inside the $() command substitution are nested. # This looks wrong to a C programmer, but it is correct in shells. printf '%sn' "The matching line is: $(grep foo "$filename")" # ^---------^ inner layer (quotes) # ^^--------------------^ middle layer (command sub) # ^---------------------------------------------^ outer layer (quotes)
An example showing an array being «joined» with «${a[*]}»:
# bash ip=192.168.1.30 netmask=255.255.254.0 IFS=. read -ra ip_octets <<<"$ip" IFS=. read -ra netmask_octets <<<"$netmask" for i in 0 1 2 3; do ((ip_octets[i] &= netmask_octets[i])) done IFS=.; network="${ip_octets[*]}"; unset IFS
The ansi-c $’…’ quoting style is used to interpret backslash escapes:
IFS=$' tn' # sets the IFS variable to the three-byte string containing # a space, a tab, and a newline
Examples showing backslash sequences:
# These are equivalent: printf '%sn' hi $'theren' # ksh / zsh: print 'hintheren'
Patterns
The shell uses quotes to suppress interpretation of special syntax within patterns and regular expressions, so that any literal or expanded string may be easily included in a pattern matching context without individually escaping each character.
if [[ $path = foo* ]]; then # unquoted foo* acts as a pattern if [[ $path = "foo*" ]]; then # quoted "foo*" is a literal string if [[ $path =~ $some_re ]]; then # the contents of $some_re are treated as a POSIX extended regular expression. if [[ $path =~ "$some_re" ]]; then # the contents of $some_re are treated as a literal string # despite the =~ operator
# the "quoted" branch is taken. g=a*[b] case $g in $g) echo 'unquoted pattern' ;; "$g") echo 'quoted pattern' esac
Here Documents
Quote-removal never applies to the contents of a here document
$ arr=(words in array); cat <<EOF > These are the "${arr[*]}" > EOF These are the "words in array"
Quoting or escaping the «delimiter» in the heredoc redirection affects whether its contents are subject to expansions. The quoted variety is bash’s only context in which an arbitrary string may be used with no special interpretation except for lines beginning with the delimiter, which marks the end of the heredoc.
{ printf '%sn' before "$(</dev/stdin)" after; } <<EOF # Anything but "EOF" can go here EOF
See also
-
Arguments
-
http://www.grymoire.com/Unix/Quote.html
-
http://wiki.bash-hackers.org/syntax/quoting
-
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02
CategoryShell