Завжди використовуйте подвійні лапки підстановок змінних і команд замін: "$foo"
,"$(foo)"
Якщо ви використовуєте без $foo
котирування, ваш скрипт буде задихатися від вхідних даних або параметрів (або виведення команди, з $(foo)
), що містять пробіл або \[*?
.
Там ви можете перестати читати. Ну добре, ось ще кілька:
read
- Щоб читати вхідні рядки за рядком із read
вбудованим, використовуйте "while IFS= read -r line; do …
Звичайна" read
обробка нахилів та пробілів спеціально.
xargs
- Уникайтеxargs
. Якщо потрібно використовувати xargs
, зробіть це xargs -0
. Замість того find … | xargs
, вважають за кращеfind … -exec …
.
xargs
обробляє пробіли та персонажів \"'
спеціально.
Ця відповідь відноситься до оболонок Bourne / POSIX-стилі ( sh
, ash
, dash
, bash
, ksh
, mksh
, yash
...). Користувачі Zsh повинні пропустити його і прочитати кінець статті Коли потрібно подвійне цитування? замість цього. Якщо ви хочете всю азотно-зернисту, прочитайте стандарт або посібник з оболонки.
Зауважте, що пояснення, наведені нижче, містять кілька наближень (тверджень, які є правдивими в більшості умов, але на них може впливати навколишній контекст або конфігурація).
Чому мені потрібно писати "$foo"
? Що відбувається без лапок?
$foo
не означає "прийняти значення змінної foo
". Це означає щось набагато складніше:
- Спочатку візьміть значення змінної.
- Розбиття полів: трактуйте це значення як список розділених пробілом, і складіть отриманий список. Наприклад, якщо змінна містить
foo * bar
то результатом цього кроку є список 3-елемент foo
, *
, bar
.
- Генерація імен файлів: трактуйте кожне поле як глобус, тобто як шаблон шаблону, і замініть його списком імен файлів, що відповідають цьому шаблону. Якщо шаблон не відповідає жодному файлу, він залишається незмінним. У нашому прикладі це призводить до списку, що містить
foo
, слідом за списком файлів у поточному каталозі, і нарешті bar
. Якщо поточний каталог порожній, результат foo
, *
, bar
.
Зауважте, що результат - це список рядків. У синтаксисі оболонки є два контексти: контекст списку та контекстний рядок. Розбиття полів та генерація імен файлів відбуваються лише у контексті списку, але це найбільше часу. Подвійні лапки розмежовують контекст рядка: весь рядок з подвійним цитуванням - це один рядок, який не слід розділяти. (Виняток: "$@"
розгорнутись до списку позиційних параметрів, наприклад "$@"
, еквівалентно, "$1" "$2" "$3"
якщо є три позиційні параметри. Див. Яка різниця між $ * і $ @? )
Те саме відбувається з підстановкою команд на $(foo)
або з `foo`
. З іншого боку, не використовуйте `foo`
: його правила котирування дивні та не портативні, а всі сучасні оболонки підтримують, $(foo)
що абсолютно рівно, за винятком інтуїтивних правил цитування.
Виведення арифметичної підстановки також зазнає тих же розширень, але це, як правило, не викликає занепокоєння, оскільки воно містить лише символи, що IFS
не розширюються (якщо не містити цифр або -
).
Див. Коли потрібно подвійне цитування? для отримання більш детальної інформації про випадки, коли ви можете опустити цитати.
Якщо ви не маєте на увазі, щоб ця ригмарола відбулася, просто пам’ятайте, що завжди використовуйте подвійні лапки навколо змінних та підстановок команд. Будьте обережні: відмова від котирувань може призвести не лише до помилок, але до дірок у безпеці .
Як обробити список імен файлів?
Якщо ви пишете myfiles="file1 file2"
з пробілами для розділення файлів, це не може працювати з іменами файлів, що містять пробіли. Імена файлів Unix можуть містити будь-який символ, окрім /
(який завжди є роздільником каталогів) та нульовими байтами (які ви не можете використовувати в скриптах оболонок з більшістю оболонок).
Така ж проблема і з myfiles=*.txt; … process $myfiles
. Коли ви це зробите, змінна myfiles
містить 5-символьний рядок *.txt
, і саме тоді, коли ви пишете $myfiles
, підстановочний знак розгортається. Цей приклад насправді буде працювати, поки ви не зміните свій сценарій myfiles="$someprefix*.txt"; … process $myfiles
. Якщо someprefix
встановлено значення final report
, це не працюватиме.
Щоб обробити список будь-якого типу (наприклад, імена файлів), покладіть його в масив. Для цього потрібні mksh, ksh93, yash або bash (або zsh, у яких немає всіх цих питань котирування); звичайна оболонка POSIX (наприклад, зола або тире) не має змінних масивів.
myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"
Ksh88 має змінні масиву з іншим синтаксисом присвоєння set -A myfiles "someprefix"*.txt
(див. Змінну призначень у різних середовищах ksh, якщо вам потрібна портативність ksh88 / bash). Оболонки стилю Bourne / POSIX мають один єдиний масив, масив позиційних параметрів, з "$@"
якими ви встановлюєте set
і який є локальним для функції:
set -- "$someprefix"*.txt
process -- "$@"
Що з іменами файлів, які починаються з -
?
У відповідній примітці пам’ятайте, що імена файлів можуть починатися з -
(тире / мінус), який більшість команд трактує як позначення параметра. Якщо у вас є ім'я файлу, яке починається зі змінної частини, не забудьте передати --
його, як у фрагменті вище. Це вказує на команду, що вона дійшла до кінця параметрів, тому що-небудь після цього - це ім'я файлу, навіть якщо воно починається з -
.
Крім того, ви можете переконатися, що імена ваших файлів починаються із символу, відмінного від -
. Абсолютні імена файлів починаються з /
, і ви можете додати ./
на початку відносні імена. Наступний фрагмент перетворює вміст змінної f
в "безпечний" спосіб перегляду того самого файлу, який гарантовано не починається -
.
case "$f" in -*) "f=./$f";; esac
На завершальну примітку до цієї теми слід пам’ятати, що деякі команди інтерпретують -
як значення стандартного вводу або стандартного виводу навіть після --
. Якщо вам потрібно посилатися на фактичний файл з назвою -
, або якщо ви викликаєте таку програму, і ви не хочете, щоб вона читала зі stdin або писала в stdout, переконайтеся, що перезаписали, -
як зазначено вище. Див. Яка різниця між "du -sh *" та "du -sh ./*"? для подальшого обговорення.
Як зберігати команду в змінній?
"Команда" може означати три речі: ім'я команди (ім'я як виконуваний файл з повним шляхом або без нього, або ім'я функції, вбудованого чи псевдоніма), ім'я команди з аргументами або фрагмент коду оболонки. Відповідно існують різні способи зберігання їх у змінній.
Якщо у вас є ім'я команди, просто збережіть його і використовуйте змінну з подвійними лапки як завжди.
command_path="$1"
…
"$command_path" --option --message="hello world"
Якщо у вас є команда з аргументами, проблема така ж, як і зі списком назв файлів вище: це список рядків, а не рядок. Ви не можете просто заповнити аргументи в одну рядок з пробілами між ними, тому що, якщо ви зробите це, ви не можете сказати різницю між просторами, що входять до складу аргументів, і пробілами, які розділяють аргументи. Якщо у вашій оболонці є масиви, ви можете їх використовувати.
cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"
Що робити, якщо ви використовуєте оболонку без масивів? Ви все ще можете використовувати позиційні параметри, якщо ви не проти змінити їх.
set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"
Що робити, якщо вам потрібно зберегти складну команду оболонки, наприклад, з перенаправленнями, трубами тощо? Або якщо ви не хочете змінювати позиційні параметри? Потім ви можете побудувати рядок, що містить команду, і використовувати eval
вбудований.
code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"
Зверніть увагу на вкладені лапки у визначенні code
: одиничні цитати '…'
розмежовують літеральний рядок, так що значенням змінної code
є рядок /path/to/executable --option --message="hello world" -- /path/to/file1
. eval
Вбудований говорить оболонку , щоб розібрати рядок , передану в якості аргументу , як якщо б він з'явився в сценарії, так що в цей момент котирування і труби розібрані і т.д.
Використання eval
складне. Подумайте уважно про те, що розбирається коли. Зокрема, ви не можете просто вписати ім'я файлу у код: вам потрібно його навести, як і у файлі вихідного коду. Немає прямого способу зробити це. Що - щось на зразок code="$code $filename"
перерв , якщо ім'я файлу містить будь - яких оболонки спеціальних символів (пропуски, $
, ;
, |
, <
, >
і т.д.). code="$code \"$filename\""
все ще ламається "$\`
. Навіть code="$code '$filename'"
перерви, якщо ім'я файлу містить a '
. Є два рішення.
Додайте шар лапки навколо імені файлу. Найпростіший спосіб зробити це - додати навколо нього одиничні лапки та замінити окремі лапки на '\''
.
quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
code="$code '${quoted_filename%.}'"
Зберігайте розширення змінної всередині коду, щоб воно було шукано при оцінці коду, а не тоді, коли створений фрагмент коду. Це простіше, але працює лише в тому випадку, якщо змінна все ще існує з тим самим значенням під час виконання коду, а не, наприклад, якщо код вбудований у цикл.
code="$code \"\$filename\""
Нарешті, чи дійсно вам потрібна змінна, що містить код? Найприродніший спосіб надати ім'я кодовому блоку - це визначити функцію.
Що з цим read
?
Без -r
, read
дозволяє рядки продовження - це єдиний логічний рядок введення:
hello \
world
read
розбиває рядок введення на поля, відмежовані символами у $IFS
(без -r
, зворотна косою рисою також уникає цих). Наприклад, якщо введення - це рядок, що містить три слова, то read first second third
встановлюється first
перше слово введення, second
друге слово і third
третє слово. Якщо є більше слів, остання змінна містить усе, що залишилося після встановлення попередніх. Провідна та задні пробіли оброблені.
Встановлення IFS
порожнього рядка дозволяє уникнути обрізки. Дивіться, чому так часто використовується "while IFS = read" замість `IFS =; поки читати..`? для більш тривалого пояснення.
Що не так xargs
?
Формат введення - xargs
це рядки, розділені пробілом, які необов'язково можуть бути одно- або подвійними. Жоден стандартний інструмент не виводить цей формат.
Вхід xargs -L1
або xargs -l
майже список списку рядків, але не зовсім - якщо в кінці рядка є пробіл, наступний рядок є рядком продовження.
Ви можете використовувати, xargs -0
де це можливо (і коли це доступно: GNU (Linux, Cygwin), BusyBox, BSD, OSX, але це не в POSIX). Це безпечно, оскільки нульові байти не можуть відображатися в більшості даних, зокрема в іменах файлів. Щоб створити розділений з нулем список імен файлів, використовуйте find … -print0
(або ви можете використовувати, find … -exec …
як пояснено нижче).
Як обробити файли, знайдені користувачем find
?
find … -exec some_command a_parameter another_parameter {} +
some_command
має бути зовнішньою командою, це не може бути функцією оболонки чи псевдонімом. Якщо вам потрібно викликати оболонку для обробки файлів, зателефонуйте sh
прямо.
find … -exec sh -c '
for x do
… # process the file "$x"
done
' find-sh {} +
У мене є ще одне питання
Перегляньте тег цитування на цьому веб-сайті або оболонку або оболонку-скрипт . (Клацніть на "дізнатися більше ...", щоб побачити деякі загальні поради та вибраний вручну список поширених питань.) Якщо ви шукали і не можете знайти відповідь, запитайте .
shellcheck
допоможуть вам покращити якість своїх програм.