Double quote to prevent globbing and word splitting

SC2086 – ShellCheck Wiki

See this page on GitHub


Problematic code:

echo $1
for i in $*; do :; done # this one and the next one also apply to expanding arrays.
for i in $@; do :; done

Correct code:

echo "$1"
for i in "$@"; do :; done # or, 'for i; do'

Rationale

The first code looks like «print the first argument». It’s actually «Split the first argument by IFS (spaces, tabs and line feeds). Expand each of them as if it was a glob. Join all the resulting strings and filenames with spaces. Print the result.»

The second one looks like «iterate through all arguments». It’s actually «join all the arguments by the first character of IFS (space), split them by IFS and expand each of them as globs, and iterate on the resulting list». The third one skips the joining part.

Quoting variables prevents word splitting and glob expansion, and prevents the script from breaking when input contains spaces, line feeds, glob characters and such.

Strictly speaking, only expansions themselves need to be quoted, but for stylistic reasons, entire arguments with multiple variable and literal parts are often quoted as one:

$HOME/$dir/dist/bin/$file        # Unquoted (bad)
"$HOME"/"$dir"/dist/bin/"$file"  # Minimal quoting (good)
"$HOME/$dir/dist/bin/$file"      # Canonical quoting (good)

When quoting composite arguments, make sure to exclude globs and brace expansions, which lose their special meaning in double quotes: "$HOME/$dir/src/*.c" will not expand, but "$HOME/$dir/src"/*.c will.

Note that $( ) starts a new context, and variables in it have to be quoted independently:

echo "This $variable is quoted $(but this $variable is not)"
echo "This $variable is quoted $(and now this "$variable" is too)"

Exceptions

Sometimes you want to split on spaces, like when building a command line:

options="-j 5 -B"
[[ $debug == "yes" ]] && options="$options -d"
make $options file

Just quoting this doesn’t work. Instead, you should have used an array (bash, ksh, zsh):

options=(-j 5 -B) # ksh88: set -A options -- -j 5 -B
[[ $debug == "yes" ]] && options=("${options[@]}" -d)
make "${options[@]}" file

or a function (POSIX):

make_with_flags() {
  [ "$debug" = "yes" ] && set -- -d "$@"
  make -j 5 -B "$@"
}
make_with_flags file

To split on spaces but not perform glob expansion, POSIX has a set -f to disable globbing. You can disable word splitting by setting IFS=''.

Similarly, you might want an optional argument:

debug=""
[[ $1 == "--trace-commands" ]] && debug="-x"
bash $debug script

Quoting this doesn’t work, since in the default case, "$debug" would expand to one empty argument while $debug would expand into zero arguments. In this case, you can use an array with zero or one elements as outlined above, or you can use an unquoted expansion with an alternate value:

debug=""
[[ $1 == "--trace-commands" ]] && debug="yes"
bash ${debug:+"-x"} script

This is better than an unquoted value because the alternative value can be properly quoted, e.g. wget ${output:+ -o "$output"}.


Here are two common cases where this warning seems unnecessary but may still be beneficial:

cmd <<< $var         # Requires quoting on Bash 3 (but not 4+)
: ${var=default}     # Should be quoted to avoid DoS when var='*/*/*/*/*/*'

As always, this warning can be ignored on a case-by-case basis.


ShellCheck is a static analysis tool for shell scripts. This page is part of its documentation.

I’m running shellcheck on my scripts and often get this warning (which in this case is correct, because cd foo bar baz makes no sense):

cd ${SOME_DIR} || exit 1
   ^-- SC2046: Quote this to prevent word splitting.

This warning is mostly a good one. One exception when the variable contains multiple arguments:

gcc ${OPTIONS} ...
    ^-- SC2046: Quote this to prevent word splitting.

Is there a convention for being more explicit about intentional word splitting, possibly avoiding this shellcheck warning?

ulidtko's user avatar

ulidtko

14.5k10 gold badges56 silver badges87 bronze badges

asked Jun 29, 2020 at 11:49

Andreas's user avatar

12

Simply add double quotes when no split is the intent:

cd "${SOME_DIR}" || exit 1

Perform explicit splitting into an array:

read -ra gcc_options <<<"${OPTIONS}"
gcc "${gcc_options[@]}"

Or disable shellcheck for the next statement, indicating you reviewed the operation as conform to the intent:

# shellcheck disable=SC2046 # Intended splitting of OPTIONS
gcc ${OPTIONS}

Sometimes Reading The Fine Manual is a better option than asking here:

Shellcheck gives links to its Wiki for code inspection warnings. The SC2046 Quote this to prevent word splitting wiki entry already mentions the use of read -a in Bash and how to disable this code inspection for specific cases with non-Bash shell grammars.

answered Jun 29, 2020 at 12:40

Léa Gris's user avatar

Léa GrisLéa Gris

17k4 gold badges32 silver badges41 bronze badges

5

Apparently, shellcheck does not complain about the missing quotes for variables (SC2046 or SC2086), if typing unquoted ${ARGS} using parameter expansion format:

${ARGS:+ $ARGS}

(The space after :+ is for readability).

answered May 1, 2022 at 13:20

Noam Manos's user avatar

Noam ManosNoam Manos

14.5k3 gold badges83 silver badges85 bronze badges

In your script, comments of the form # shellcheck disable=... will disable a particular warning.

options="a b c"
# shellcheck disable=2086
foo $options

If you are running the shellcheck script locally, you can use the -e option instead of adding directives to the script.

$ cat tmp.sh
#/bin/sh

options="a b c"

foo $options
$ shellcheck tmp.sh

In tmp.sh line 5:
foo $options
    ^------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
foo "$options"

For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
$ spellcheck -e SC2086 foo.sh
$

answered Jun 29, 2020 at 12:38

chepner's user avatar

chepnerchepner

487k70 gold badges509 silver badges666 bronze badges

In any cases you showed, there is no reason not to quote expansions. Use quotes.

Is there a convention for being more explicit about intentional word splitting, possibly avoiding this shellcheck warning?

The convention would be to perform word splitting with mapfile or read -a.

If you really want to use word splitting, then a convention would be to add a comment explaining the reason why your code wants to depend on word splitting and then additionally you could add a warning:

# I use word splitting here, because...
# shellcheck disable=SC2046

to disable the shellcheck warning, see shellcheck/wiki/ignore.

Note: Use lower case variables in your scripts. By convention, upper case variables are used for exported variables, like PATH PWD UID COLUMNS LINES etc.

answered Jun 29, 2020 at 12:44

KamilCuk's user avatar

KamilCukKamilCuk

114k7 gold badges54 silver badges98 bronze badges

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement. We’ll occasionally send you account related emails.

Already on GitHub?
Sign in
to your account

Comments

@sahil-lakhwani

Rule Id: SC2086
shellcheck -V : 0.5.0

I am using this compliance script.

shellcheck complains at the below line:
set -- $p

error: SC2086: Double quote to prevent globbing and word splitting.

The problem is that if I put a double quote around $p, the set command, I guess, interprets in a different way. Hence, the value of $1 is computed same as of $p instead of the first token from $p.

@matthewpersico

Correct. With quotes, $p is one string, with spaces embedded. The solution is to add a shellcheck disable directive at the line above; that’s what they are for.

The REAL solution is to change the assignment to $p so that $p is an array. I am surprised that shellcheck didn’t complain about that assignment.

@koalaman

In an array=( $p ) you would have gotten a better warning «Quote to prevent word splitting, or split robustly with mapfile or read -a.»

Maybe set -- $p should have a similar warning when the shell is bash, and be ignored in sh.

@sahil-lakhwani

@matthewpersico

declare -a p
readarray -t p < <(echo "$PATH" | sed -e 's/::/:/' -e 's/:$//' -e 's/:/n/g')
set -- "${p[@]}"

Things to note:

  1. The naive assignment p=($(echo $PATH | sed -e 's/::/:/' -e 's/:$//' -e 's/:/ /g')) fails any time an individual element of the PATH has a space in it, usually in Windows. Using the readarray properly keeps such paths as one element.
  2. Furthermore, the change of : to n instead of ‘ ‘ is made because readarray is line-based. It becomes more obvious when you lookup up readarray in the bash man page and see that it is just a synonym for ‘mapfile’.

@sahil-lakhwani

@vapier

should close this bug then ? sounds like shellcheck is correct and the warning is WAI.

@philippgl

I think this is a bug / should be fixed.
If you want to set a single variable, you would use var="$p" with a nice variable name.

As the set -- is used for the positional args ($1, $2, …), I would assume 99% of the times, you want the splitting. I think, this could be a warning in bash or other nice shells with arrays.

I only use this in shells w/o arrays, where this is the only «array» available and I would to place the output of some command into the pos. args like in set -- $(cmd...).

@vapier

the linter is correct: the code written set -- $p undergoes globbing/splitting and needs quoting. if you don’t want it to warn, disable the warning in your code.

@philippgl

I know, that set -- $p does the splitting and globbing. I think, this is the main reason to do the set -- $p in the first place. set -- $p is used to assign multiple variables, right?
So for the only (?) legitimate use of set --, I always have to disable the warning?
If you do set --"$p", it just assigns the named variable $p to $1.

@vapier

i’ve seen POSIX code like:

set -- "${first_arg}"
if <some condition>; then
  set -- "$@" --some-arg
fi

the fact that it’s difficult to dtrt doesn’t mean we should make the linter silent. set -- $p is objectively wrong as the linter is correctly calling out. you can disable path expansion via set, and silence the linting warning for the one line as documentation that you want the word splitting.

@philippgl

Can I disable this w/o changing the file itself?
I do not want to clutter my source code with linter specific stuff. Probably I also need to do this for each linter?
The only option would be to pass the --exclude=SC2056, which would disable it for the whole file, right?

@vapier

correct, --exclude affects the whole file, but does not require modification of it

ftrader

pushed a commit
to bitcoin-cash-node/bitcoin-cash-node
that referenced
this issue

May 29, 2022

@slowriot

@ftrader

Some small adjustments to the git-subtree-check.sh script to silence code
quality warnings from codeclimate / shellcheck.

- double-quote variables to avoid globbing and splitting where appropriate
- avoid possible interpretation of string as options in set
- suppress shellcheck warning SC2086 ("Double quote to prevent globbing and
  word splitting") for set directives where this is intended behaviour
  (see bug koalaman/shellcheck#1284)

Behaviour of the script is unchanged.

This change eliminates the last of the shellcheck warnings in the codebase (*)

Test plan
---------

To run the codeclimate checks manually, use:

```
docker run --interactive --tty --rm --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze
```

(*) Note: there are some shellcheck warnings for these globbings in
depends/, src/univalue and build-aux, if you have built in your tree.
But there should not be any instances in src/ , test/ , doc/ or contrib/ .

You need to use "$(somecmd "$file")".

Without the quotes, a path with a space will be split in the argument to somecmd, and it will target the wrong file. So you need quotes on the inside.

Any spaces in the output of somecmd will also cause splitting, so you need quotes on the outside of the whole command substitution.

Quotes inside the command substitution have no effect on the quotes outside of it. Bash’s own reference manual isn’t too clear on this, but BashGuide explicitly mentions it. The text in POSIX also requires it, as «any valid shell script» is allowed inside $(...):

With the $(command) form, all characters following the open parenthesis to the matching closing parenthesis constitute the command. Any valid shell script can be used for command, except a script consisting solely of redirections which produces unspecified results.


Example:

$ file="./space here/foo"

a. No quotes, dirname processes both ./space and here/foo:

$ printf "<%s>n" $(dirname $file)
<.>
<here>

b. Quotes inside, dirname processes ./space here/foo, giving ./space here, which is split in two:

$ printf "<%s>n" $(dirname "$file")
<./space>
<here>

c. Quotes outside, dirname processes both ./space and here/foo, outputs on separate lines, but now the two lines form a single argument:

$ printf "<%s>n" "$(dirname $file)"
<.
here>

d. Quotes both inside and outside, this gives the correct answer:

$ printf "<%s>n" "$(dirname "$file")"
<./space here>

(that would possibly have been simpler if dirname only processed the first argument, but that wouldn’t show the difference between cases a and c.)

Note that with dirname (and possibly others) you also need want to add --, to prevent the filename from being taken as an option in case it happens to start with a dash, so use "$(dirname -- "$file")".

Bash-скрипты — эффективное решение для автоматизации рутинных задач, но не всегда самое простое. Объемные сценарии характеризуются низкой производительностью и сложны для чтения. В этой статье мы рассмотрим, как оптимизировать работу, упростить с помощью утилит sed и awk и не совершать очевидных ошибок в написании скриптов. 

Настройка выполнения скриптов

Управление процессами в Linux увеличивает коэффициент полезного использования ЦП, памяти, устройств ввода-вывода и других составляющих системы. Рассмотрим, как применить эти принципы и команды при запуске bash-скриптов в работу. 

Скорость работы сценария зависит от количества операций и их «энергозатратности» для ресурсов системы. Проблема в том, что во время его выполнения системный администратор не может использовать консоль.  Как решить эту задачу? 

  1. Использовать терминальные мультиплексоры: screen или более продвинутый tmux, о которых слышали даже «новички» в Unix-среде. Программы позволяют разделить терминал, создав несколько окон и поддерживая несколько сессий одновременно. 

  2. Перемещать процесс выполнения  скрипта в фоновый режим. Если скрипт еще не запущен, то добавьте амперсанд & в конце команды

$ ./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

I ran a bash script I have written through ShellCheck and one of the things it flagged was this:

Line 61:
      GPIO[$i]="$(</sys/class/gpio/gpio$i/value)"
                                       ^-- [SC2086](https://github.com/koalaman/shellcheck/wiki/SC2086): Double quote to prevent globbing and word splitting.

Did you mean:
      GPIO[$i]="$(</sys/class/gpio/gpio"$i"/value)"

This statement is part of a loop that reads the status of GPIO pins 22 through 25 on Raspberry Pi, so $i will always be one of 22, 23, 24, or 25, and GPIO[$i] will therefore be either 0 or 1. I created a short test script to test the above line both with and without the quotes around $i, and got the same result either way, but it seems a bit counter-intuitive to me to add the quotes in this case, because it would seem like quoting the entire string «$(</sys/class/gpio/gpio$i/value)» would prevent globbing and that then putting additional quotes around the «$i» would actually take it out of the quoted bits and allow globbing. Is ShellCheck crazy, or am I misunderstanding how this works?

Вам нужно использовать "$(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")".

As software engineers, we always strive to follow best practices when developing our projects. By that I mean, with each code commit or pull request we like to run automated checks to ensure the quality or security of our code.

But occasionally, some Bash or shell script with code which has been copy-pasted from the internet finds its way into the project – and these scripts inevitably behave unexpectedly when tested across a range of different scenarios.

That’s where ShellCheck comes into its own – and it could potentially save you a heck of a lot of time in your day-to-day software development.

What is ShellCheck ?

ShellCheck is a Bash or sh script linting tool that can be used to detect the quality violations – and ensure that scripts follow the best practices.

Here are some of the main uses of ShellCheck:

  • It detects various types of inaccurate quoting
  • It can identify many incorrect conditional statements
  • It recognizes the misuse of commands
  • It detects syntax errors
  • It make suggestions for improving the robustness of the script
  • It warns you about portability issues
  • And many more uses

How to use it – common lines and what they mean

I have run the ShellCheck in one of our repositories and found the following lines. Here are my suggestions for what to do with them:

  • echo “FAILED: $cmd”
    • SC2154: cmd is referenced but not assigned.
    • This is an indication that you should double check the variable name  – and make sure it’s being spelt correctly.
  • for test in $folder/*.py; do
    • SC2231: Quote expansions in this for loop glob to prevent wordsplitting, e.g. “$dir”/*.txt .
    • When iterating over globs containing expansions, make sure glob characters are outside quotes. For example, “$dir/*.txt” will not glob expand, but “$dir”/*.txt or “$dir”/*.”$ext” will.
  • while read line
    • SC2162: read without -r will mangle backslashes.
    • By default, read will interpret backslashes before spaces and line feeds, and otherwise strip them. This is rarely expected or desired. You should make sure that is what you intended.
  • if echo $line | grep -F = &>/dev/null
    • SC2086: Double quote to prevent globbing and word splitting.
    • Quoting variables prevents the script from breaking when input contains spaces, line feeds, or glob characters.
  • if [ ! -z “$single_test” ]; then
    • SC2236: Use -n instead of ! -z.
    • You have negated test -z or test -n, resulting in a needless double-negative. This is a syntax issue that doesn’t affect the overall correctness of the script.

How to suppress ShellCheck warnings

If you think that the warning or issue suggested by the ShellCheck isn’t important or valid (for instance, anything related to styling), you can suppress them.

To do this, you can add a comment inside the shell file:

hexToAscii() {# shellcheck disable=SC2059printf "x$1"}

Or extend the SHELLCHECK_OPTS variable with ignored checks:

export SHELLCHECK_OPTS="-e SC2059 -e SC2034 -e SC1090"

How to install and run ShellCheck in Travis CI

By default, ShellCheck isn’t installed in Travis CI Loading…virtual machines or containers. But it is very easy to do so. Simply follow these steps:

First, add this line into your .travis.yml file:

dist: trustyaddons:apt:packages:- shellcheck

Then, in order to run ShellCheck with each build trigger, you can add this line into the .travis.yml file:

script:- find . -name "*.sh" -print0 | xargs -n 1 -0 shellcheck

And that’s it. Don’t you just love simple fixes?

Conclusion

In this blog, we introduced the ShellCheck linting tool for Bash and shell scripts. We hope this guide helps you write shell scripts which conform to best practices and have consistent syntax – so they’re always easy to maintain, robust and portable. That way, if anyone mentions Ghost in the Shell when you’re coding you can happily refer them to Manga.

Понравилась статья? Поделить с друзьями:
  • Double line spaced word
  • Download pdf to word or excel
  • Double letter word with a
  • Download open office excel
  • Double letter spelling word list