Підводні камені Bash



У цій статті ми поговоримо про помилки, які здійснюються програмістами на Bash. У всіх наведених прикладах є якісь вади. Вам вдасться уникнути багатьох з нижчеописаних помилок, якщо ви завжди будете використовувати лапки і ніколи не будете використовувати розбиття на слова (wordsplitting)! Розбиття на слова — це збиткова легасі-практика, успадкована з оболонки Bourne. Вона використовується за замовчуванням, якщо ви не укладаєте підстановки (expansions) в лапки. Загалом, переважна більшість підводних каменів так чи інакше пов'язані з підстановкою без лапок, що призводить до розбиття на слова і глоббингу (globbing) отриманого результату.

Зміст
  1. for i in $(ls *.mp3)

  2. cp $file $target
  3. Імена файлів з попередніми дефісами
  4. [ $foo = «bar» ]
  5. cd $(dirname "$f"
  6. [ "$foo" = bar && "$bar" = foo ]
  7. [[ $foo > 7 ]]
  8. grep foo bar | while read -r; do ((count++)); done
  9. if [grep foo myfile]
  10. if [bar="$foo"]; then ...
  11. if [ [ a = b ] && [ c = d ] ]; then ...
  12. read $foo
  13. cat file | sed s/foo/bar/ > file
  14. echo $foo
  15. $foo=bar
  16. foo = bar
  17. echo <<EOF
  18. su -c 'some command'
  19. cd /foo; bar
  20. [ bar == "$foo" ]
  21. for i in {1..10}; do ./something &; done
  22. cmd1 && cmd2 || cmd3
  23. echo «Hello World!»
  24. for arg in $*
  25. function foo()
  26. echo "~"
  27. local varname=$(command)
  28. export foo=~/bar
  29. sed 's/$foo/good bye/'
  30. tr [A-Z] [a-z]
  31. ps ax | grep gedit
  32. printf "$foo"
  33. for i in {1..$n}
  34. if [[ $foo = $bar ]] (залежно від мети)
  35. if [[ $foo =~ 'some RE' ]]
  36. [ -n $foo ] or [ -z $foo ]
  37. [[ -e "$broken_symlink" ]] повертає 1, незважаючи на існування $broken_symlink
  38. Збій ed file <<<«g/d\{0,3\}/s//e/g»
  39. Збій подцепочки (sub-string) expr для «match»
  40. Про UTF-8 і відмітках послідовності байтів (Byte-Order Marks, BOM)
  41. content=$(<file)
  42. for file in ./*; do if [[ $file != *.* ]]
  43. somecmd 2>&1 >>logfile
  44. cmd; ((! $? )) || die
  45. y=$(( array[$x] )
  46. read num; echo $((num+1))
  47. IFS=, read -ra fields <<< "$csv_line"
  48. export CDPATH=.:~/myProject


1. for i in $(ls *.mp3)
Одна з найпоширеніших помилок, що здійснюються BASH-програмістами. Виражається вона в написанні подібних циклів:

for i in $(ls *.mp3); do # Неправильно!
some command $i # Неправильно!
done

for i in $(ls) # Неправильно!
for i in `ls` # Неправильно!

for i in $(find . -type f) # Неправильно!
for i in `find . -type f` # Неправильно!

files=($(find . -type f)) # Неправильно!
for i in ${files[@]} # Неправильно!

Так, було б чудово, якби ви могли обробляти вихідні дані
ls
або
find
у вигляді списку імен файлів і итерировать його. Але ви не можете. Цей підхід цілком помилковий, і цього ніяк не виправити. Потрібно підходити до цього зовсім інакше.

Тут є як мінімум п'ять проблем:

  1. Якщо ім'я файлу містить прогалини, то воно підлягає WordSplitting. Припустимо, у поточній папці у нас є файл з ім'ям
    01 - don't Eat the Yellow Snow.mp3
    . Цикл
    for
    итерирует кожне слово і видасть результат: 01, -, don't, Eat, etc.

  2. Якщо ім'я файлу містить символи glob, то воно підлягає глоббингу ("globbing"). Якщо вихідні дані
    ls
    містить символ *, те слово, в яке він входить, буде розцінено як шаблон і замінено списком імен файлів, що йому відповідають. Шлях до файла може містити будь-які символи, за винятком NUL. Так, в тому числі і символи перекладу рядка.

  3. Утиліта
    ls
    може пошматувати імена файлів. В залежності від платформи, на якій виробите, від використовуваних вами аргументів (або не використовуються), а також в залежності від того, вказують на термінал стандартні вихідні дані,
    ls
    може раптово замінити якісь символи в імені файлу на "?". Або взагалі їх не виводити. Ніколи не намагайтеся парсити вихідні дані ls.

  4. CommandSubstitution обрізає з вихідних даних всі кінцеві символи переносу рядка. На перший погляд, це добре, тому що
    ls
    додає новий рядок. Але якщо останнє ім'я файлу у списку закінчується новим рядком, то `...` або
    $()
    приберуть і його в додачу.
Також не можна укладати підстановку в подвійні лапки:

for i in "$(ls *.mp3)"; do # Неправильно!

Це призведе до того, що вихідні дані
ls
цілком будуть вважатися одним словом. Замість итерирования кожного імені файлу, цикл буде виконано один раз, присвоївши
i
рядкове значення з об'єднаних імен файлів. І ви не можете просто змінити IFS на новий рядок. Імена файлів можуть містити нові рядки.

Інша варіація на цю тему полягає у зловживанні розбиттям на слова і циклами for (неправильного) читання рядків файла. Наприклад:

IFS=$'\n'
for line in $(cat file); do ... # Неправильно!

Це не працює! Особливо якщо рядки є іменами файлів. Bash (як і будь-яка інша оболонка сімейства Bourne) просто не працює таким чином. В додаток до всьому сказаному, зовсім не потрібно використовувати саму
ls
. Це зовнішня команда, вихідні дані якої спеціально призначені для читання людиною, а не для парсингу скриптом.

Так як робити правильно?

Використовуєте
find
, наприклад, в сукупності з
exec
:

find . -type f -exec some command {} \;

Замість
ls
можна розглянути такий варіант:

for i in *.mp3; do # Вже краще! і...
some command "$i" # ...завжди укладайте в подвійні лапки!
done

Оболонки POSIX, як і Bash, спеціально для цього мають властивість globbing — це дозволяє їм застосовувати шаблони до списку подібних імен файлів. Не потрібно інтерпретувати результати роботи зовнішньої утиліти. Оскільки globbing — останній етап процедури підстановки, то шаблон
*.mp3
коректно застосовується до окремих слів, на які не чинить ефекту підстановка без лапок. Якщо вам потрібно рекурсивно обробляти файли, то скористайтеся UsingFind або придивіться до
shopt -s globstar
в Bash 4 і вище.

Запитання: Що станеться, якщо в поточній папці немає файлів, які відповідають шаблону *.mp3? Цикл
for
буде виконано один раз з
i=".*.mp3"
, що не є очікуваною поведінкою! В якості вирішення цієї проблеми можна застосовувати перевірку на наявність відповідного файлу:

# POSIX
for i in *.mp3; do
[ -e "$i" ] || continue
some command "$i"
done

Інше рішення — використовувати властивість Bash'а
shopt -s nullglob
. Хоча так можна робити тільки після прочитання документації та уважної оцінки ефекту цієї установки на всі інші glob'и в цьому скрипті. Зверніть увагу на лапки
$i
в тілі циклу. Це приводить нас до другої проблеми:.

2. cp $file $target
Що поганого в цій команді? В принципі, нічого, якщо ви заздалегідь знаєте, що
$file
та
$target
не містять пропусків або символів символів (wildcards). Однак результати підстановки все одно піддаються WordSplitting і підстановці шляху до файлу. Тому завжди укладайте параметричні підстановки (parameter expansions) в подвійні лапки.

cp -- "$file" "$target"

В іншому випадку ви отримаєте таку команду
cp 01 - don't Eat the Yellow Snow.mp3 /mnt/usb
, що призведе до помилок на зразок
cp: cannot stat `01': No such file or directory
. Якщо
$file
містить символи підстановки (* або ? або [), то вони будуть розкладені, тільки якщо є задовольняють умовам файли. З подвійними лапками все буде добре, поки на початку "$file" не виявиться символу "-". У цьому випадку
cp
вирішить, що ви намагаєтеся згодувати йому опції командного рядка (див. наступну главу).

Навіть в кілька незвичайних обставинах, коли ви можете гарантувати вміст змінної, лапки підстановки параметрів — це хороша та загальноприйнята практика, особливо якщо в них містяться імена файлів. Досвідчені автори скриптів завжди будуть використовувати лапки, за винятком рідкісних випадків, коли з контексту код абсолютно очевидно, що параметр містить гарантовано безпечне значення. Експерти напевно вирішать, що використання в заголовку команди
cp
є помилкою. Вам теж варто так вважати.

3. Імена файлів із попередніми дефісами
Імена файлів із попередніми дефісами можуть доставити чимало проблем. Glob'и зразок
*.mp3
відсортовані в розширений список (expanded list) (згідно вашої поточної локалі), а в більшості локалей спочатку сортується дефіс, а потім літери. Потім список передається якійсь команді, яка може неправильно інтерпретувати
filename
в якості опції. У цій ситуації є два основних рішення.

Перше — вставити два дефіса (
--
) між командою (наприклад
cp
) і її аргументами. Це буде сигналом припинення пошуку опцій, і все буде добре:

cp -- "$file" "$target"

Але у цього підходу є свої проблеми. Ви повинні бути впевнені, що вставляєте
--
кожному параметра в контексті, коли він може бути інтерпретований в якості опції. А це передбачає велику надмірність, і можна легко щось упустити.

Більшість з добре написаних бібліотек для парсингу опцій це розуміють, і коректно використовують їх програми повинні безкоштовно наслідувати цю особливість. Однак майте на увазі, що відповідальність за розпізнавання закінчень опцій лежить виключно на додатку. Деякі програми, які парсят опції вручну, або роблять це некоректно, або використовують сторонні бібліотеки можуть закінчення розпізнавати. Стандартні утиліти повинні це робити, не рахуючи кількох винятків, описаних у POSIX. Наприклад,
echo
.

Інший варіант — упевнитися, що ваші імена файлів завжди починаються з папки. Для цього використовуються відносні або абсолютні шляхи до файлів.

for i in ./*.mp3; do
cp "$i" /target
...
done

У такому випадку, навіть якщо у нас є файл, ім'я якого починається з дефіса, завдяки glob ми можемо бути впевнені, що змінна завжди містить щось на зразок
./-foo.mp3
. А це абсолютно безпечно, якщо говорити про
cp
.

Нарешті, якщо ви можете гарантувати, що всі результати будуть мати однаковий префікс, і змінна використовується в тілі циклу лише кілька разів, то можете просто конкатенувати префікс при підстановці. Теоретично, заощадить нам генерування і зберігання кількох додаткових символів для кожного слова.

for i in *.mp3; do
cp "./$i" /target
...
done

4. [ $foo = «bar» ]
Ця ситуація дуже схожа на проблему, описану в другій главі. Але все ж я повторю її, оскільки вона дуже важлива. У наведеній В заголовку рядку лапки знаходяться не там, де потрібно. В Bash вам не потрібно лапки рядкові літерали (якщо вони не містять метасимволи або символи шаблонів). Але ви повинні лапки свої змінні, якщо вони можуть містити пробіли або символи підстановки.

Наведений приклад може зламатися з кількох причин:

  • Якщо змінна, на яку посилаються в
    [
    , не існує або вона порожня, тоді команда
    [
    в кінцевому підсумку буде виглядати так:

    [ = "bar" ] # Неправильно!
    

    … і викине помилку:
    unary operator expected.
    (Оператор
    =
    є двійковим, а не унарным, тому команда
    [
    буде шокована від зустрічі з ним).

  • Якщо змінна містить внутрішні прогалини, то вона буде розділена на слова до того, як її побачить команда
    [
    . Отже, отримаємо:

    [ multiple words here = "bar" ]
    

    Можливо ви не бачите проблем, але завдяки використанню
    [
    тут присутня синтаксична помилка. Правильний спосіб написання:

    # POSIX
    [ "$foo" = bar ] # Правильно!
    

    Це буде прекрасно працювати в сумісних з POSIX реалізаціях, навіть якщо перед
    $foo
    буде йти дефіс, тому що в POSIX-команда
    [
    визначає свої дії в залежності від кількості переданих їй аргументів. Тільки зовсім давні оболонки будуть відчувати з цим проблеми, можете про них не переживати при написанні коду (див. далі виверт з x"
    $foo
    ").
В Bash та багатьох інших ksh-подібних оболонках є чудова альтернатива, яка використовує ключове слово [[.

# Bash / Ksh
[[ $foo == bar ]] # Правильно!

Вам не потрібно брати в лапки посилання на змінні, розташовані зліва від
=
[[ ]]
, тому що вони не зазнають поділу на слова або глоббингу. І навіть порожні змінні будуть коректно оброблені. З іншого боку, використання лапок ніяк не зашкодить. На відміну від
[
та
test
, ви також можете використовувати
==
. Тільки зверніть увагу, що при порівняннях з використанням
[[
пошук за шаблоном виконується для рядків в правій частині, а не просте порівняння рядків. Щоб зробити праву рядок литералом, ви повинні помістити її в лапки, при використанні будь-яких символів, які мають особливе значення в контексті пошуку за шаблоном.

# Bash / Ksh
match=b*r
[[ $foo == "$match" ]] # Добре! Якщо лапок не буде, то також буде порівняна за шаблоном b*r.

Ймовірно, ви бачили такий код:

# POSIX / Bourne
[ x"$foo" = xbar ] # Можна, але зазвичай не потрібно.

Для коду, що працює на зовсім древніх оболонках, зажадає хак x"
$foo
". Тут замість
[[
використовується більш примітивне
[
. Якщо
$foo
починається з дефіса, то виникає плутанина. На старих системах
[
не піклується про те, починається з дефіса токен праворуч від
=
. Вона використовує його буквально. Так що потрібно бути більш уважними з лівою частиною.

Зверніть увагу, що оболонки, для яких потрібен такий обхідний шлях, не сумісні з POSIX. Навіть Heirloom Bourne цього не вимагає (ймовірно, це неРОЅІХ клон Bourne-оболонки, який до цих пір є однією з найпоширеніших системних оболонок). Така екстремальна портируемость затребувана рідко, вона робить ваш код менш читабельним і красивим.

5. cd $(dirname "$f")
Ще одна помилка, пов'язана з лапками. Як і у випадку з підстановкою змінної (variable expansion), результат підстановки команди піддається розбиття на слова і підстановці шляху до файлу. Тому укладайте в лапки:

cd -P -- "$(dirname -- "$f")"

Тут не зовсім очевидна логіка вкладеності лапок. Програміст буде очікувати, що перші і другі подвійні лапки будуть згруповані разом, а потім будуть йти треті і четверті. Але в Bash все інакше. Bash обробляє лапки всередині підстановки команди як одну пару, а подвійні лапки зовні підстановки — як іншу пару.

Можна написати й по-іншому: парсер обробляє підстановку команди як «рівень вкладеності», і лапки всередині йдуть окремо від лапок зовні.

6. [ "$foo" = bar && "$bar" = foo ]
Не можна використовувати
&&
всередині старої команди test (або [). Парсер Bash бачить
&&
зовні
[[ ]]
або
(( ))
, і в результаті розбиває вашу команду дві команди — до і після
&&
. Замість цього використовуйте один з двох варіантів:

[ bar = "$foo" ] && [ foo = "$bar" ] # Правильно! (POSIX)
[[ $foo = bar && $bar = foo ]] # Теж правильно! (Bash / Ksh)

(Зверніть увагу, що за причини легасі, згаданого в главі 4, ми поміняли місцями константу і змінну всередині
[
. Можна було б поміняти і
[[
, але для запобігання інтерпретування в якості шаблону довелося б брати підстановки в лапки).

Те ж саме відноситься і до
||
. Замість них використовуйте
[[
або дві команди
[
.

Уникайте такого:

[ bar = "$foo" -a foo = "$bar" ] # Не портируемо.

Бінарні оператори
a
,
o
( / ) (групування) — це XSI-розширення стандарту POSIX. У POSIX-2008 всі вони позначені як застарілі. Використовувати їх в новому коді не варто. Однією з практичних проблем, пов'язаних з
[ A = B -a C = D ]
або
o
), є те, що POSIX не визначає результати команд
test
або
[
з більш ніж чотирма аргументами. Ймовірно, у більшості оболонок це буде працювати, але розраховувати на це не можна. Якщо вам потрібно писати для POSIX-оболонки, то використовуйте дві команди
test
або
[
, розділені оператором
&&
.

7. [[ $foo > 7 ]]
Тут є кілька моментів. По-перше, команду
[[
не слід використовувати виключно для обчислення арифметичних виразів. Її потрібно застосовувати для test-виразів, що включають один з підтримуваних test-операторів. Хоча технічно ви можете виконувати обчислення з допомогою операторів
[[
, але робити це має сенс лише у поєднанні з одним із нематематичних test-операторів, який присутній десь у виразі. Якщо ви хочете всього лише порівняти числові дані (або виконати будь-яке інше арифметична дія), то набагато краще застосувати
(( ))
:

# Bash / Ksh
((foo > 7)) # Правильно!
[[ foo -gt 7 ]] # Працює, але безглуздо. Багато вважатимуть помилкою. Краще використовувати ((...)) або let.

Якщо всередині
[[ ]]
ви використовуєте оператор >, то система опрацює це як порівняння строкових даних (перевірка порядку сортування по локалі), а не числових. Іноді це може спрацювати, але підведе вас саме тоді, коли ви найменше очікуєте. Ще гірше використовувати > всередині
[ ]
: це перенаправлення виводу. У вашій папці з'явиться файл з назвою 7, і тест буде успішно виконуватися до тих пір, поки в
$foo
.

Якщо потрібно сувора сумісність з POSIX і не доступна команда
((
, тоді правильною альтернативою буде використання старомодною
[
:

# POSIX
[ "$foo" -gt 7 ] # Теж правильно!
[ $((foo > 7)) -ne 0 ] # Сумісний з POSIX еквівалент (( для більш загальних математичних операцій.

Зверніть увагу, що якщо
$foo
не є цілим числом, то команда
test ... -gt
завершиться невдало. Тому лапки має сенс тільки заради продуктивності і поділу аргументів на поодинокі слова, щоб знизити ймовірність виникнення побічних ефектів у деяких оболонках.

Якщо ви не можете гарантувати вхідні дані для будь-якого арифметичного контексту (включаючи
((
або
let
), або тестового вираження
[
, який передбачає числові порівняння, тоді ви повинні завжди валідувати вхідні дані, перш ніж виконувати обчислення.

# POSIX
case $foo in
*[![:digit:]]*)
printf '$foo expanded to a non-digit: %s\n' "$foo" >&2
exit 1
;;
*)
[ $foo -gt 7 ]
esac

8. grep foo bar | while read -r; do ((count++)); done
Цей код виглядає нормально? Звичайно, це всього лише посередня реалізація
grep -c
, але так зроблено для простоти прикладу. Зміни в
count
не будуть поширюватися за межі циклу
while
, тому що кожна команда конвеєра виповнюється в окремій подоболочке (SubShell). У якийсь момент це дивує будь-якого новачка в Bash.

POSIX не визначає, чи повинен обчислюватися в подоболочке останній елемент конвеєра. Одні оболонки, начебто ksh93 і Bash >= 4.2 із включеним
shopt -s lastpipe
, запустять наведений у прикладі цикл
while
у вихідному shell-процесі, що може призвести до будь-яких побічних ефектів. Отже, портируемые скрипти повинні писатися так, щоб не залежати від подібної поведінки.

Способи вирішення цієї та подібних проблем ви можете почерпнути з Bash FAQ #24. Тут їх занадто довго описувати.

9. if [grep foo myfile]
У багатьох новачків виникає помилкове уявлення про виразах
if
, обумовлене тим, що дуже часто за цим ключовим словом відразу йде
[
або
[[
. Люди вважають, що
[
якимось чином є частиною синтаксису вираження
if
, як і прості дужки, які у вираженні
if
в мові С. Це не так!
if
одержує пункт. Нею є
[
, це не синтаксичний маркер для
if
. Ця команда еквівалентна
test
, за винятком того, що останнім аргументом повинен бути
]
. Наприклад:

# POSIX
if [ false ]; then echo "HELP"; fi
if test false; then echo "HELP"; fi

Ці рядки еквівалентні: обидві перевіряють, щоб аргумент «false» не був порожнім. В обох випадках буде виводитися HELP, на подив програмістів, які прийшли з інших мов і намагаються розібратися з синтаксисом оболонки.

У вираження
if
такий синтаксис:

if COMMANDS
then <COMMANDS>
elif <COMMANDS> # optional
then <COMMANDS>
else <COMMANDS> # optional
fi # required

Ще раз —
[
є командою. Вона отримує аргументи, як і будь-яка інша звичайна команда.
if
— це складова команда, що містить інші команди. І в її синтаксисі немає [!

Хоча в Bash є вбудована команда
[
, і таким чином він знає про
[
,
]
немає нічого особливого. Bash лише передає
]
в якості аргументу команді
[
, якій потрібно, щоб саме
]
був останнім аргументом, інакше скрипт буде виглядати непривабливо.

Там може бути нуль і більше опціональних секцій
elif
, а також одна опціональна секція
else
.

Складова команда
if
містить дві і більше секцій, в яких знаходяться списки команд. Кожна секція починається з ключового слова
then
,
elif
або
else
, а закінчується ключовим словом
fi
. Код завершення останньої команди першої секції і кожна наступна секція
elif
визначають обчислення кожної відповідної секції
then
. Інша секція
elif
обчислюється до того, як буде виконана одна з
then
. Якщо не обчислено ні однієї секції
then
, то відбувається перемикання на гілку
else
. Якщо немає
else
, то блок
if
завершується, а результуюча команда
if
повертає 0 (true).

Якщо ви хочете прийняти рішення в залежності від вихідних даних команди
grep
, то не потрібно укладати її в круглі або квадратні дужки, backticks або будь-який інший синтаксис! Просто використовуйте
grep
як команду після
if
:

if grep -q fooregex myfile; then
...
fi

Якщо
grep
знаходить збіг у рядку
myfile
, тоді код завершення буде 0 (true), і виконається частина
then
. Якщо збігів знайдено не буде,
grep
повертає значення, відмінне від 0, а результуюча команда
if
буде нулем.

Читайте також:

10. if [bar="$foo"]; then ...
[bar="$foo"] # Неправильно!
[ bar="$foo" ] # Все ще неправильно!

Як зазначено в попередній главі,
[
— це команда (це можна довести за допомогою
type -t [
або
whence -v [
). Як і у випадку з будь-якою іншою простою командою, Bash очікує, що після неї буде йти пробіл, потім перший аргумент, знову пробіл, і так далі. Ви просто не можете нехтувати пробілами! Ось правильне написання:

if [ bar = "$foo" ]; then ...

Кожний з компонентів —
bar
,
=
, підстановка "
$foo
" і
]
— є окремими аргументами команди
[
. Кожна пара аргументів повинна бути розділена пробіл, щоб оболонка знала, де починається і закінчується кожен з них.

11. if [ [ a = b ] && [ c = d ] ]; then ...
Повторюся вкотре.
[
є командою. Це не синтаксичний маркер, розташований між
if
і яким-небудь «станом», на зразок як у С. Не використовується
[
для групування. Ви не можете взяти З-команди
if
і транслювати їх у Bash-команди, просто замінивши круглі дужки на квадратні!

Якщо ви хочете висловити складові умовні конструкції, робіть так:

if [ a = b ] && [ c = d ]; then ...

Зверніть увагу, що тут у нас дві команди після
if
, об'єднані оператором
&&
(логічне AND, скорочене обчислення). Це те ж саме, що:

if test a = b && test c = d; then ...

Якщо перша команда
test
повертає false, то вхід в тіло вираження
if
не виконується. Якщо повертає true, тоді запускається друга команда
test
; якщо й вона повертає true, тоді виконується вхід в тіло вираження
if
. (C-програмісти вже знайомі з
&&
. Bash використовує таке ж спрощене обчислення. Подібно до того, як
||
виконує спрощене обчислення операції OR.)

Ключове слово
[[
дозволяє використання
&&
, так що можна написати і так:

if [[ a = b && c = d ]]; then ...

У розділі 6 описана проблема, пов'язана з комбінуванням
test
з умовними операторами.

12. read $foo
Не використовуйте
$
перед ім'ям змінної у команді
read
. Якщо ви хочете помістити дані в змінну з ім'ям
foo
, робіть так:

read foo

Або ще безпечніше:

IFS= read -r foo

read $foo
вважає рядок вхідних даних і помістить її в змінну/ті з ім'ям
$foo
. Це може бути корисним, якщо ви дійсно хотіли зробити
foo
посиланням на іншу змінну; але в більшості випадків це баг.

13. cat file | sed s/foo/bar/ > file
Ви не можете читати з файлу і писати в нього в рамках одного конвеєра. В залежності від того, що робить ваш конвеєр, файл:

  • може виявитися затертий (clobbered) (до 0 байт, або до розміру, еквівалентного розміру буфера конвеєра вашої ОС),
  • може розростися і заповнити всі доступний дисковий простір,
  • може досягти граничного розміру, заданого ОС або вами, і так далі.
Якщо ви хочете безпечно змінити файл, не просто додаючи інформацію в кінець, то скористайтеся текстових редактором.

printf %s\\n ',s/foo/bar/g' w q | ed -s file

Якщо він не може допомогти вам у вирішенні вашої задачі, то в певний момент(*) необхідно створити тимчасовий файл.

Цей приклад можна перенести без обмежень:

sed 's/foo/bar/g' file > tmpfile && mv tmpfile file

Цей приклад буде працювати тільки під GNU sed 4.x:

sed -i 's/foo/bar/g' file(s)

Зверніть увагу, що тут створюється тимчасовий файл і застосовується такий же трюк з перейменуванням — обробка виконується прозоро.

А наступна команда-аналог вимагає наявності Perl 5.x (який, ймовірно, більш поширений, ніж GNU sed 4.x):

perl -pi -e 's/foo/bar/g' file(s)

За подробицями про заміну вмісту файлів зверніться до Bash FAQ #21.

(*) у мануалі до
sponge
з moreutils наводиться такий приклад:

sed '...' file | grep '...' | sponge file

Замість використання тимчасового файлу і атомарного
mv
, ця версія «вбирає» (цитата з мануала!) всі дані, перш ніж відкрити і записати в
file
. Правда, якщо програма або система завалиться в ході операції, дані будуть втрачені, тому що в цей момент на диску немає копії вихідної інформації.

Використання тимчасового файлу
+ mv
все ще піддає нас невеликого ризику втрати даних при падінні системи / відключенні живлення. Щоб старий або новий файл зберігся, потрібно перед
mv
sync
.

14. echo $foo
Ця відносно нешкідливо виглядає команда вносить сильну плутанину. Оскільки
$foo
не була взята в лапки, вона не тільки піддасться розбиття на слова, але і глоббингу. З-за цього Bash-програмісти думають, що їх змінні містять неправильні значення, хоча насправді з ними все в порядку. Вносить Смуту розбиття на слова або підстановка шляху до файлу.

msg="будь Ласка, введіть назву форми *.zip"
echo $msg

Це повідомлення розбито на слова, а все глоби (glob) (начебто *.zip) розкладені. Що подумають користувачі, коли побачать повідомлення:
будь Ласка, введіть назву форми freenfss.zip lw35nfss.zip
. Ілюстрація:

var=".*.zip" # var містить зірочку, точку і слово "zip"
echo "$var" # пише *.zip
echo $var # пише список файлів, що закінчуються на .zip

По суті, тут команда
echo
не може бути використана безпечно. Якщо змінна містить, наприклад,
n
,
echo
вирішить, що це опція, а не дані для виводу на екран. Єдиний гарантований спосіб виведення значення змінної — використання
printf
:

printf "%s\n" "$foo"

15. $foo=bar
Ні, поміщаючи
$
перед ім'ям змінної, ви не привласнюєте їй значення. Це не Perl.

16. foo = bar
Ні, ви не можете вставити прогалини навколо
=
, коли привласнюєте значення змінної. Це не Ц. Коли ви пишете
foo = bar
, оболонка розбиває це на три слова. Перше —
foo
— береться в імені команди. Друге і третє — в якості аргументів команди.

  • foo= bar # Неправильно!
  • foo =bar # Неправильно!
  • $foo = bar; # ЗОВСІМ НЕПРАВИЛЬНО!
  • foo=bar # Правильно.
  • foo="bar" # Ще правильніше.
17. echo <<EOF
Here-док — це корисний інструмент для вбудовування в скрипт великих блоків текстових даних. Це призводить до спрямування рядків тексту в скрипті на стандартний ввід команди. На жаль, команда
echo
не читає з stdin.

# Це неправильно:
echo <<EOF
Hello world
How's it going?
EOF

# Ось що ви намагалися зробити:
cat <<EOF
Hello world
How's it going?
EOF

# Або використовуйте лапки, які можуть об'єднувати кілька рядків (ефективно, echo вбудована):
echo "Hello world
How's it going?"

При використанні лапок це буде прекрасно працювати у всіх оболонках. Але ви не зможете просто закинути в скрипт пачку рядків. Перша і остання рядки повинні мати синтаксичну розмітку. Якщо ви хочете, щоб рядки не містили синтаксису оболонки, і не хочете множити команду
cat
, то скористайтеся альтернативою:

# Або застосуйте printf (теж ефективно, printf вбудована):
printf %s "\
Hello world
How's it going?
"

У прикладі з
printf
, знак \ у першій рядку запобігає поява додаткової нового рядка початку текстового блоку. Нова рядок в кінці блоку (тому що остання лапки знаходиться в новому рядку). Відсутність
\n
в рядку
printf
запобігає додавання в кінці нового рядка. Лише трюк з \ не спрацює при використанні одинарних лапок. Якщо ви хочете включити в них блок тексту, то у вас є два варіанти, і обидва вони мають на увазі «забруднення» ваших даних синтаксисом оболонки:

printf %s \
'Hello world
'

printf %s 'Hello world
'

18. su -c 'some command'
Цей синтаксис майже коректний. Проблема в тому, що на багатьох платформах
su
бере аргумент
c
, але не той, який вам потрібен. Ось приклад з OpenBSD:

$ su -c 'echo hello'
su: only the superuser may specify a login class

Ви хочете передати оболонці
c 'some command'
, тобто
c
вам треба ім'я користувача.

su root -c 'some command' # Now it's right.

su
означає ім'я root користувача, коли ви опускаєте його. Але він стикається з цим, коли ви пізніше намагаєтеся передати команду оболонці. Так що в цьому випадку ви повинні явно вказати ім'я користувача.

19. cd /foo; bar
Якщо ви не перевіряєте на наявність помилок після команди
cd
, то можете виконати
bar
в неправильному місці. А це пахне катастрофою, якщо, наприклад,
bar
виявиться
rm -f *
. Завжди перевіряйте на наявність помилок після команди
cd
. Найпростіший спосіб:

cd /foo && bar

Якщо після
cd
йде більше однієї команди, то можна зробити так:

cd /foo || exit 1
bar
baz
bat ... # Lots of commands.

cd
повідомить про неможливість зміни папки, видавши stderr-повідомлення на кшталт «bash: cd: /foo: No such file or directory». Якщо ви хочете додати stdout власне повідомлення, то можете застосувати групування команд:

cd /net || { echo >&2 "can't read /net. Make sure you've logged in to the Samba network, and try again."; xit 1; }
do_stuff
more_stuff

Зверніть увагу, що між
{
та
echo
потрібен пробіл. Також перед закриваючою
}
потрібен
;
.

Деякі люблять включати set -e, щоб скрипти переривалися на будь-якій команді, яка повертає значення, відмінне від нуля. Але це не так просто використовувати правильно (бо багато звичайні команди можуть повертати не нульове значення задля попередження, що ви можете не вважати фатальним).

До речі, якщо ви багато разів змінюєте папки в Bash-скрипті, то почитайте інструкцію по користуванню
pushd
,
popd
та
dirs
. Ймовірно, весь код, який ви писали для управління
cd
та
pwd
, абсолютно не потрібен. Порівняйте це:

find ... -type d -print0 | while IFS= read -r -d " subdir; do
here=$PWD
cd "$subdir" && whatever && ...
cd "$here"
done

C цим:

find ... -type d -print0 | while IFS= read -r -d " subdir; do
(cd "$subdir" || exit; whatever; ...)
done

Примусове використання подоболочки змушує
cd
виконуватися тільки в ній. Для наступної ітерації циклу ми повертаємося в нормальне місце, незалежно від того, чи успішно виконалася
cd
. Нам не потрібно міняти папки вручну, і ми не застреваем в нескінченній рядку з логікою
... && ...
, що запобігає використання інших умовних конструкцій. Версія з подоболочкой простіше і чистіше (хоча і трошки повільніше).

20. [ bar == "$foo" ]
Оператор
==
не є валідним для POSIX-команди
[
. Використовуйте
=
ключове слово
[[
.

[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes

В Bash
[ "$x" == y ]
приймається як підстановка, тому багато програмістів вважають синтаксис правильним. Але це не так — це «башизм» (Bashism). Якщо ви зібралися використовувати башизмы, то можете замість цього використовувати
[[
.

21. for i in {1..10}; do ./something &; done
Не поміщати
;
відразу після
&
. Просто видаліть зайву
;
.

for i in {1..10}; do ./something & done
Або: 
for i in {1..10}; do
./something &
done

&
вже працює як переривач команди (command terminator), як і
;
. Не можна їх змішувати.

В цілому,
;
можна замінити новим рядком, але не всі нові рядки можна замінити на
;
.

22. cmd1 && cmd2 || cmd3
Хтось любить використовувати
&&
та
||
в якості скороченого синтаксису для
if ... then ... else ... fi
. У багатьох випадках це безпечно:

[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."

Проте в цілому ця конструкція не повністю еквівалентна
if fi ... 
. Команда, що йде після
&&
, також генерує код завершення. І якщо цей код не «істина» (0), тоді буде викликана і команда, що йде після
||
. Наприклад:

i=0
true && ((i++)) || ((i--))
echo $i # Prints 0

Що тут відбувається? Схоже, що
i
має дорівнювати 1, але виходить 0. Чому? Тому що були виконані і
i++
та
i--
. Команда
((i++))
має код завершення, який успадкований від обчислення виразу всередині круглих дужок за прикладом мови С. Значення виразу дорівнює 0 (початкове значення
i
), а З вираження цілим числом, рівним 0, вважається false. Так що команда
((i++))
(коли
i
0) має код завершення 1 (false), і означає також виконується команда
((i--))
.

Це не відбувається, якщо ми використовуємо оператор попереднього инкрементирования, оскільки код завершення
++i
дорівнює true:

i=0
true && (( ++i )) || (( --i ))
echo $i # Prints 1

Але це працює завдяки випадковості. Ви не можете покладатися на
x && y || z
, якщо y має найменший шанс збою! Цей приклад не буде працювати, якщо початкове значення
i
-1 замість 0.

Якщо вас хвилює безпека або якщо ви просто не впевнені, як це працює, або якщо ви хоч щось незрозуміли з попередніх параграфів, будь ласка, використовуйте простий синтаксис
if fi ... 
.

i=0
if true; then
((i++))
else
((i--))
fi
echo $i # Prints 1

Ця частина також застосовна до оболонкою Bourne:

true && { echo true; false; } || { echo false; true; }

На виході виходить два рядки «true» та «false», замість одного рядка «true».

23. echo «Hello World!»
В інтерактивній оболонці Bash (до версії 4.3), ви побачите помилку:

bash: !": event not found

Справа в тому, що при налаштуваннях за замовчуванням для інтерактивної оболонки Bash виконує підстановку історії (history expansion) в стилі csh, використовуючи знак оклику. Це проблема не для скриптів оболонки, а тільки для інтерактивних оболонок. На жаль, очевидна спроба «виправлення» не спрацює:

$ echo "hi\!"
hi\!

Найпростіше рішення — повернути в початковий стан опцію
histexpand
. Це можна зробити за допомогою
set +H
або
set +o histexpand
:

Запитання: Чому краще використовувати
histexpand
, ніж одинарні лапки?

Я особисто зіткнувся з цією ситуацією, коли маніпулював файлами пісень з допомогою команд на кшталт

mp3info -t "don't Let It Show" ...
mp3info -t "Ah! Leah!" ...

Одинарні лапки незручні у використанні, тому що всі пісні мають у назвах апострофи. Використання подвійних лапок призвело до заміщення історії. А уявіть, що в файлу у назві і апостроф, і подвійні лапки. Так що від лапок краще відмовитися. Оскільки я ніколи не вдаюся до підстановці історії, то волію вимкнути
~/.bashrc. --
GreyCat

Спрацює таке рішення:

echo 'Hello World!'

Або

set +H
echo "Hello World!"

Або

histchars=

Багато хто просто кладуть
set +H
або
set +o histexpand
в свої
~/.bashrc
, щоб назавжди деактивувати підстановку історії. Це справа смаку, вибирайте, що вам більше підходить.

Інше рішення:

exmark='!'
echo "Hello, world$exmark"

В Bash 4.3 і нижче, подвійні лапки після
!
не запускають підстановку історії. Але з подвійними лапками воно все ж виконується, і хоча з
echo "Hello World!"
порядок, у нас ще є проблема:

echo "Hello, World!(and the rest of the Universe)"
echo "foo!'bar'"

24. for arg in $*
Bash (як і всі Bourne-оболонки) має спеціальний синтаксис для посилання на список позиційних параметрів, по одному за раз. Це
$*
, а не
$@
. Вони обидва розкладаються на список слів у ваших параметри скрипта, кожен параметр не є окремим словом. Правильний синтаксис:

for arg in "$@"

# Або простіше:
for arg

Оскільки в скриптах часто проганяють через цикли позиційні параметри,
for arg
за замовчуванням використовується для
for arg in "$@"
. Взятий у лапки
"$@"
— це особлива магія, завдяки якій кожен параметр використовується як окреме слово (або окрема ітерація циклу). Так ви повинні робити в 99% випадків.

Приклад:

# Помилкова версія
for x in $*; do
echo "параметр: '$x'"
done

$ ./myscript 'arg 1' arg2 arg3
параметр: 'arg'
параметр: '1'
параметр: 'arg2'
параметр: 'arg3'

Треба було написати:

# Правильна версія
for x in "$@"; do
echo "параметр: '$x'"
done
# Або краще:
for x; do
echo "параметр: '$x'"
done

$ ./myscript 'arg 1' arg2 arg3
параметр: 'arg 1'
параметр: 'arg2'
параметр: 'arg3'

25. function foo()
У деяких оболонках це працює, але не у всіх. При визначенні функції ніколи не комбінуйте ключове слово
function
з круглими дужками
()
. Bash (як мінімум деякі версії) дозволяє їх змішувати. Але більшість оболонок такий код не візьмуть (наприклад, zsh 4.x і, ймовірно, вище). Деякі оболонки візьмуть
function foo
, але для максимальної сумісності краще застосовувати:

foo() {
...
}

26. echo "~"
Підстановка з допомогою тільди застосовується тільки тоді, коли '~' не взято в лапки. У цьому прикладі echo пише в stdout '~', а не шлях користувача домашньої теки. Брати в лапки параметри, які виражені відносно користувача домашньої папки, потрібно з допомогою $HOME, а не '~'. Візьміть ситуацію, коли $HOME — це "/home/my photos".

"~/dir with spaces" # розгортається до "~/dir with spaces"
~"/dir with spaces" # розгортається до "~/dir with spaces"
~/"dir with spaces" # розгортається до "/home/my photos/dir with spaces"
"$HOME/dir with spaces" # розгортається до "/home/my photos/dir with spaces"

27. local varname=$(command)
При оголошенні функції локальної змінної,
local
самостійно діє як команда. Іноді інша частина рядка може итерироваться дивно. Наприклад, якщо ви хочете отримати код завершення (
$?
) підстановки команди, то у вас нічого не вийде. Він буде приховано кодом завершення локалі. Для цього краще розділяти команди:

local varname
varname=$(command)
rc=$?

Наступна проблема описує іншу особливість синтаксису.

28. export foo=~/bar
Коли тільда знаходиться на початку слова, — самостійно або через слеш — то гарантовано буде виконана тільки підстановка з допомогою тільди (з ім'ям користувача або без нього). Також воно обов'язково буде виконано, коли в присвоєнні тільда йде відразу після
=
.

Однак команди
export
та
local
не здійснюють присвоювання. Так що в деяких оболонках (зразок Bash)
export foo=~/bar
піддасться підстановці з допомогою тільди, а в інших (зразок dash) — немає.

foo=~/bar; export foo # Правильно!
export foo="$HOME/bar" # Правильно!

29. sed 's/$foo/good bye/'
В одинарних лапках параметри підстановки зразок
$foo
не розкладаються. Це призначення одинарних лапок — захищати від оболонки символи на зразок
$
. Застосовуйте подвійні лапки:

foo="привіт"; sed "s/$foo/good bye/"

Але пам'ятайте: у цьому випадку вам може знадобитися використовувати більше escapes. За подробицями зверніться до сторінкилапки».

30. tr [A-Z] [a-z]
Тут як мінімум три проблеми. Перша проблема:
[A-Z]
та
[a-z]
розглядаються як оболонкою глоби. Якщо у вас в поточній папці немає файлів з іменами, що складаються з одного символу, то команда виходить некоректною. Якщо є, то все піде шкереберть. Ймовірно, в 3 ночі у вихідні.

Друга проблема: насправді це неправильна нотація для
tr
. Фактично, тут '[' переводиться в '[', потім щось з діапазону A-Z a-z, а потім ']' в ']'. Так що вам навіть не потрібні ці квадратні дужки, перша проблема зникне сама собою.

Третя проблема полягає в тому, що в залежності від локалі, A-Z або a-z можуть не дати вам очікувані 26 ASCII-символів. Фактично в деяких локалях z знаходиться посеред алфавіту! Рішення залежить від того, що вам потрібно:

# Використовуйте, якщо хочете змінити 26 латинських літер
LC_COLLATE=C tr A-Z a-z

# Використовуйте, якщо вам потрібно перетворення в залежності від локаля. Це з більшою ймовірністю потрібно користувачам
tr '[:upper:]' '[:lower:]'

Для другої команди необхідно використовувати лапки, щоб уникнути глоббинга.

31. ps ax | grep gedit
Фундаментальна проблема полягає в тому, що ім'я виконуваного процесу за своєю природою ненадійно. Може бути кілька легітимних процесів gedit. Може бути щось ще, що маскується під gedit (можна тривіально змінити объявляемое ім'я виконаної команди). Щоб розібратися докладніше, читайте про управлінні процесами. При пошуку PID gedit (наприклад), багато почнуть з

$ ps ax | grep gedit
10530 ? S 6:23 gedit
32118 pts/0 R+ 0:00 grep gedit

А це, в залежності від Race Condition, часто видає як результат сам grep. Його можна відфільтрувати:

ps ax | grep -v grep | grep gedit # спрацює, але виглядає страшненько


Альтернатива:

ps ax | grep '[g]edit' # візьміть в лапки, щоб уникнути shell GLOB

Grep буде проігноровано в таблиці процесів, тому що він
[g]edit
,
grep
будуть шукати один раз виконаний
gedit
.

У GNU/Linux параметр –C можна використовувати для фільтрування по імені команди:

$ ps -C gedit
PID TTY TIME CMD
10530 ? 00:06:23 gedit

Але навіщо переживати, якщо можна використовувати
pgrep
?

$ pgrep gedit
10530

На другому етапі PID часто витягується за допомогою
awk
або
cut
:

$ ps -C gedit | awk '{print $1}' | tail -n1

Але навіть це можна обробити за допомогою трильйонів параметрів для
ps
:

$ ps -C gedit -opid=
10530

Якщо ви застрягли в 1992 році і не використовуєте
pgrep
, то можете застосувати давній, застарілий і осуждаемый
pidof
(тільки під GNU/Linux):

$ pidof gedit
10530

А якщо вам потрібен PID, щоб вбити процес, то вас може зацікавити
pkill
. Тільки зверніть увагу, що, наприклад,
pgrep/pkill ssh
також знайде процеси під назвою sshd, вбивати які вам не захочеться.

На жаль, деякі програми починаються не зі свого імені. Скажімо, Firefox часто стартує як firefox-bin. Цей процес теж треба буде знайти за допомогою, скажімо, ps ax | grep firefox. Або можна додати кілька параметрів до pgrep:

$ pgrep -fl firefox
3128 /usr/lib/firefox/firefox
7120 /usr/lib/firefox/plugin-container /usr/lib/flashplugin-installer/libflashplayer.so -greomni /usr/lib/firefox/omni.ja 3128 true plugin

Почитайте про управління процесами. Серйозно.

32. printf "$foo"
Тут помилка пов'язана не з лапками, а з використанням рядка формату. Якщо
$foo
не знаходиться під вашим повним контролем, тоді наявність у змінній символів
\
або
%
може призвести до небажаного поведінки. Завжди ставте свою рядок формату:

printf %s "$foo"
printf '%s\n' "$foo"

33. for i in {1..$n}
Парсер Bash виконує розкриття дужок всіх розкладів і підстановок. Так що код розкриття бачить літерал
$n
, який не є числом, і тому не розкриває фігурні дужки у списку чисел. Так що практично неможливо використовувати розкриття дужок для створення списків, розмір яких відомий лише в ході runtime. Краще робіть так:

for (i=1; i < =n; i++)); do
...
done

При простому итерировании цілочисельних змінних майже завжди краще починати з циклу арифметичних обчислень
for
, а не з розкриття дужок, тому що в останньому випадку кожен аргумент попередньо розкладається (pre-expands), що може знизити продуктивність і збільшити споживання пам'яті.

34. if [[ $foo = $bar ]] (залежно від мети)
Якщо не взяти в лапки те, що розташоване праворуч від
=
[[, то Bash не буде вважати це рядком, а буде зіставляти з шаблоном. Так що якщо у наведеному коді
bar
міститиме *, то результат буде завжди true. Якщо ви хочете перевірити рядкові на еквівалентність один одному, то візьміть праву частину в лапки:

if [[ $foo = "$bar" ]]

Якщо вам потрібно зіставити з шаблоном, то розумніше буде вибрати інше ім'я змінної вказує, що права частина містить шаблон. Або використовувати коментарі.

Ще потрібно зазначити, що якщо взяти в лапки частину праворуч від
=~
, тоді буде виконуватися примусово і просте порівняння строкових, а не тільки зіставлення регулярних виразів. Це приводить нас до наступної проблеми.

35. if [[ $foo =~ 'some RE' ]]
Лапки навколо правої частини роблять їх вміст рядком, а не регулярним виразом. Якщо вам потрібно використовувати довгий або складний регулярний вираз, уникаючи численних изолирований з допомогою зворотніх слешів, то помістіть дані в змінну:

re='some RE'
if [[ $foo =~ $re ]]

Також це допомагає обійти відмінності в роботі
=~
в різних версіях Bash. Завдяки змінної ми уникаємо деяких неприємних і неочевидних проблем.

Та ж проблема виникає при зіставленні з шаблоном всередині
[[
:

[[ $foo = ".*.glob" ]] # Wrong! *.glob is treated as a literal string.
[[ $foo = *.glob ]] # Correct. *.glob is treated as a glob-style pattern.

36. [ -n $foo ] or [ -z $foo ]
Використовуючи команду
[
, ви повинні брати в лапки всі передані їй підстановки. В іншому випадку
$foo
може розкластися на 0 слів, або 42 слова, або будь-яке інше кількість, крім 1, що зламає синтаксис.

[ -n "$foo" ]
[ -z "$foo" ]
[ -n "$(всередині якась команда з "$file")" ]

# [[ не виконує поділ на слова або підстановку з допомогою глобов, так що можна використовувати і це:
[[ -n $foo ]]
[[ -z $foo ]]

37. [[ -e "$broken_symlink" ]] повертає 1 незважаючи на існування $broken_symlink
Test йде після symlink'ів, отже, якщо symlink зламаний, — наприклад, вказує на файл, який не існує або знаходиться в недоступній папці, — тоді
test –e
повертає 1, незважаючи на існування symlink. Щоб це вирішити (і підготуватися до цього), використовуйте:

# bash/ksh/zsh
[[ -e "$broken_symlink" || -L "$broken_symlink" ]]

# POSIX sh+test
[ -e "$broken_symlink" ] || [ -L "$broken_symlink" ]

38. Збій ed file <<<«g/d\{0,3\}/s//e/g»
Проблема в тому, що ed не приймає 0 \{0,3\}. Можете перевірити, що такий код працює:

ed file <<<"g/d\{1,3\}/s//e/g"

Зверніть увагу, що це відбувається незважаючи на те що POSIX-стану, які BRE (особливість регулярних виразів, використовувана ed), повинні приймати значення 0 мінімальної кількості входжень (див. главу 5).

39. Збій подцепочки (sub-string) expr для «match»
Це досить добре працює… частіше всього

word=abcde
expr "$word" :".\(.*\)"
bcde

Але зі словом «match» відбудеться збій

word=match
expr "$word" : ".\(.*\)"

Справа в тому, що «match» — ключове слово. У GNU ця проблема вирішується за допомогою префікса '+'

word=match
expr + "$word" : ".\(.*\)"
atch

Або можна просто відмовитися від
expr
. Все, що вона вміє робити, ви можете виконувати за допомогою параметричної підстановки (Parameter Expansion). Наприклад, потрібно прибрати першу букву в слові? У POSIX-оболонках це вирішується за допомогою параметричної підстановки або розкладання на подцепочки (Substring Expansion):

$ word=match
$ echo "${word#?}" # PE
atch
$ echo "${word:1}" # SE
atch

Серйозно, для використання
expr
у вас немає виправдань, якщо тільки ви не працюєте на Solaris з його несумісним з POSIX
/bin/sh
. Це зовнішній процес, так що він працює набагато повільніше, ніж внутрипроцессная обробка рядка. А оскільки ніхто його не використовує, то ніхто і не розуміє, що він робить, так що ваш код буде заплутаний і важкий у супроводі.

40. Про UTF-8 і мітки послідовності байтів (Byte-Order Marks, BOM)
В цілому: в Unix текст у кодуванні UTF-8 не використовують BOM. Кодування звичайного тексту визначається локаллю, MIME-типами або іншими метаданими. Хоча наявність BOM зазвичай не шкодить документом в кодуванні UTF-8, призначеному тільки для читання людьми, але є проблемою (часто через нелегальний синтаксису) в будь-яких текстових файлів, призначених для автоматизованих процесів в скриптах, вихідному коді, конфігураційних файлах і так далі. Файли, які починаються з BOM, потрібно вважати чужорідними, як і ті, що містять розриви рядків в стилі MS-DOS.

В скриптах оболонок: там, де UTF-8 прозоро використовується в 8-бітних середовищах, застосування BOM буде заважати будь-якого протоколу або файловим форматом, який очікує, що на початку йтимуть конкретні ASCII-символи, на кшталт "#!" на початку скриптів Unix-оболонки».

http://unicode.org/faq/utf_bom.html#bom5

41. content=$(<file)
З цим виразом все в порядку, але пам'ятайте, що ці підстановки команд (всіх форм:
`...`
,
$(...)
,
$(<file)
,
`<file`
та
${ ...; }
(ksh)) видаляють всі кінцеві нові рядки. Найчастіше, це недоречно або навіть небажано, але якщо вам потрібно зберегти вихідні дані літерала, включаючи всі можливі кінцеві нові рядки, то це буде непросто, оскільки невідомо, чи є вони взагалі і скільки їх. Є спосіб вирішення цієї проблеми, хоча і кострубатий: додаємо всередині підстановки команди постфікс і прибираємо його зовні.

absolute_dir_path_x=$(readlink -fn -- "$dir_path"; printf x)
absolute_dir_path=${absolute_dir_path_x%x}

Менш портируемое, але більш приємне рішення: використовувати
read
з порожнім роздільником.

# Ksh (or bash 4.2+ with lastpipe enabled)
readlink -fn -- "$dir_path" | IFS= read -rd " absolute_dir_path

Недоліком цього способу є те, що
read
завжди буде повертати false, якщо команда не видає NUL-байт, що призводить до читання тільки частини потоку. Єдиний спосіб отримання коду завершення команди — через
PIPESTATUS
. Можна також навмисно виводити NUL-байт, щоб змусити
read
повернути true, і використовувати
pipefail
.

set -o pipefail
{ readlink -fn -- "$dir_path"; printf '\0x'; } | IFS= read -rd " absolute_dir_path

Тут справжній хаос портируемости: Bash підтримує
pipefail
та
PIPESTATUS
, ksh93 — тільки
pipefail
, лише останні версії mksh підтримують
pipefail
, а більш ранні — тільки
PIPESTATUS
. Крім того, потрібно найсвіжіша версія ksh93, щоб змусити
read
зупинитися на NUL-байті.

42. for file in ./*; do if [[ $file != *.* ]]
Одним із способів не дати програмами інтерпретувати передаються їм імена файлів як опції, є використання шляхів до файлів (див. главу 3). У назвах файлів у поточній папці можна використовувати префікс відносного шляху
./
.

Але можуть виникнути проблеми з шаблоном
*.*
, тому що він зіставляє файли виду
./filename
. У простому випадку можна просто напряму використовувати глоб для генерування бажаних відповідностей. Однак, якщо потрібен окремий етап зіставлення з шаблоном (наприклад, результати були попередньо оброблені і збережені в масиві, і їх потрібно відфільтрувати), то в шаблоні можна враховувати префікс:
[[ $file != ./*.* ]]
, або розщепити шаблон.

# Bash
shopt -s nullglob
for path in ./*; do
[[ ${path##*/} != *.* ]] && rm "$path"
done

# Так ще краще
for file in *; do
[[ $file != *.* ]] && rm "./${file}"
done

# Всі ще краще
for file in *.*; do
rm "./${file}"
done

Інший спосіб сигналізувати про кінець опцій — використовувати аргумент
--
(знову читайте главу 3).

shopt -s nullglob
for file in *; do
[[ $file != *.* ]] && rm -- "$file"
done

43. somecmd 2>&1 >>logfile
Безсумнівно, це найбільш поширена помилка, пов'язана з перенаправлениями, зазвичай чинена програмістами, які хочуть направити в файл або пайп stdout і stderr. Вони намагаються це зробити і не розуміють, чому stderr все ще відображається в їх терміналах. Якщо ви теж в подиві з цього приводу, то певно не знаєте, як почати працювати з перенаправлениями або файловими дескрипторами. Перенаправлення виконуються зліва направо, до виконання команди. Цей семантично неправильний код означає: «спочатку перенаправити стандартну помилку туди, куди зараз вказує стандартний висновок (tty), а потім перенаправити стандартний вивід в лог-файл». Це у зворотному напрямку. Стандартна помилка спрямовується в термінал. Правильно так:

somecmd >>logfile 2>&1

Читайте більш докладне пояснення, пояснення про дескриптор Copy і BashGuide — redirection.

44. cmd; ((! $? )) || die
$?
потрібно тільки тоді, коли ви намагаєтеся отримати конкретний статус попередньої команди. Якщо вам всього лише потрібно дізнатися, чи була вона успішною чи ні (будь-який не нульовий статус), то запитайте команду безпосередньо, наприклад:

if cmd; then
...
fi

Перевірку коду завершення за списком альтернатив можна робити за таким шаблоном:

cmd
status=$?
case $status in
0)
echo success >&2
;;
1)
echo 'Must supply a parameter, exiting.' >&2
exit 1
;;
*)
echo "Unknown error $status, exiting." >&2
exit "$status"
esac

45. y=$(( array[$x] ))
Оскільки POSIX висловлює арифметичну підстановку (arithmetic expansion) словами (розкладання підстановки команд викликається після параметричної підстановки), то підстановка індексу масиву array subscript) всередині арифметичної підстановки може призвести до впровадження експлойтів. Так, виходить багато великих, заплутаних слів.

$ x='$(date >&2)' # перенаправлення тільки для того, щоб бачити, що відбувається
$ y=$((array[$x])) # масив навіть не потрібно створювати
Mon Jun 2 10:49:08 EDT 2014

Не допоможе і використання лапок:

$ y=$((array["$x"]))
Mon Jun 2 10:51:03 EDT 2014

Працюють два способи:

# 1. Ізолювання $x, щоб він не був розкладений раніше часу.
$ y=$((array[\$x]))

# 2. Використання повного синтаксису ${array[$x]}.
$ y=$((${array[$x]}))

46. read num; echo $((num+1))
Завжди валидируйте вхідні дані (див. BashFAQ/054), перш ніж використовувати num в арифметичному контексті, оскільки він дозволяє виконувати впровадження чужорідного коду.

$ echo 'a[$(echo injection >&2)]' | bash -c 'read num; echo $((num+1))'
injection
1

47. IFS=, read -ra fields <<< "$csv_line"
Це може здатися неймовірним, але POSIX підходити до IFS як до прерывателю поля (field terminator), а не до роздільник полів. У нашому прикладі це означає, що якщо в кінці рядка введення є порожнє поле, то воно буде відкинуте:

$ IFS=, read -ra fields <<< "a,b,"
$ declare -p fields
declare -a fields='([0]="a" [1]="b")'

Куди поділося порожнє поле? Воно пропало з історичних причин («тому що завжди так робилося»). Така поведінка характерна не тільки для Bash, це роблять всі сумісні оболонки. Непорожня поле сканується коректно:

$ IFS=, read -ra fields <<< "a,b,c"
$ declare -p fields
declare -a fields='([0]="a" [1]="b" [2]="с")'

Що нам робити з цією нісенітницею? Судячи з усього, додавання IFS-символу в кінець вхідний рядки змусить сканування працювати правильно. Якщо заднє поле пусте, то додатковий IFS-символ «перериває» його, щоб воно було проскановано. А якщо заднє поле непорожня, то IFS-символ створює нове, порожнє поле, яке і втрачається.

$ input="a,b,"
$ IFS=, read -ra fields <<< "$input,"
$ declare -p fields
declare -a fields='([0]="a" [1]="b" [2]="")'

48. export CDPATH=.:~/myProject
Не експортуйте CDPATH. Налаштування CDPATH в .bashrc не є проблемою, але експортування призведе до того, що виконуваний Bash — або sh-скрипт, який використовує
cd
, може повести себе інакше. Є дві проблеми. Скрипт, який робить наступне:

cd some/dir || exit
cmd to be run in some/dir

Може змінити теку
./some/dir
на
~/myProject/some/dir
, залежно від того, які папки існують у даний момент. Так що
cd
може відправити скрипт в помилкову папку, що може зробити негативний вплив на наступні команди, які тепер виконуються не там, де задумано.

Друга проблема — коли
cd
виконується в контексті, де захоплюються вихідні дані:

output=$(cd some/dir && some command)

Коли CDPATH налаштований, то в якості побічного ефекту
cd
може видати stdout щось на зразок
/home/user/some/dir
, щоб показати, що папка знайдена через CDPATH, який завершиться у вихідний змінної з очікуваними вихідними даними
some command
.

Скрипт може набути імунітет до CDPATH, успадкованому від середовища, завдяки обов'язковому використанню
./
для відносних шляхів. Чи можна на початку скрипта запустити
unset CDPATH
, але не думайте, що кожен автор скриптів враховує цей підводний камінь, так що не експортуйте CDPATH.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.