Разбираемся с аргументами в Bash-скриптах

0
1186
views

Перевод статьи «Handling Arguments in Bash Scripts».

Создание Bash-скриптов для автоматизации набора команд — первый шаг на пути к созданию инструментов, облегчающих вашу жизнь. Даже простые скрипты, читающиеся сверху вниз и запускающиеся по установленному графику, способны сэкономить вам массу времени. Но рано или поздно наступит момент, когда вы захотите настраивать поведение вашего скрипта на лету: создавать директории с нужными вам именами, загружать файлы из определенных git-репозиториев, указывать IP-адреса или порты и т. п. Вот здесь вам и пригодятся аргументы скриптов.

Позиционные и специальные параметры

Bash предоставляет нам переменные, которые присутствуют в любом написанном нами скрипте. Вот несколько самых полезных:

$1, $2, $3, …: позиционные параметры

Позиционные параметры содержат значения, которые вы указываете при запуске своего скрипта (в командной строке) в качестве аргументов. Рассмотрим пример. Допустим, у вас есть скрипт, запускающийся следующим образом:

$ ./my_script 200 goats

Переменная $1 будет содержать значение «200», а переменная $2 — значение «goats».

Я использую позиционные параметры в одном из своих простейших скриптов. Этот скрипт я запускаю на работе практически ежедневно (здесь показываю упрощенный вариант):

!/usr/bin/env bash
project_name="$1"
mkdir -p "${project_name}/{CAD,drawings,mold,resources}"
echo "New project '$project_name' created!"

Как видите, я беру позиционную переменную $1 и сохраняю ее значение в настоящей именованной переменной. Но делать так не обязательно. Я мог бы написать

mkdir -p "$1/{CAD,drawings,mold,resources}"

и все равно все бы прекрасно работало.

Тем не менее, я предпочитаю сохранять позиционные параметры в именованные переменные вверху своего скрипта. Таким образом любой читатель моего скрипта сможет быстро понять, что к чему. Разумеется, это не заменяет хорошую документацию и надежную обработку ошибок, но это приятный маленький бонус в плане читаемости. Подобные вещи хоть немножко, но помогают, так что это хорошая практика.

Когда я запускаю скрипт вот так:

$ new_project "catheter-01"

он генерирует структуру директорий:

catheter-01
|- CAD
|- drawings
|- mold
|- resources

$0: имя скрипта

В позиционной переменной $0 при вызове скрипта сохраняется его имя. Это особенно полезно для вывода сообщений о том, как нужно использовать этот скрипт.

#!/usr/bin/env bash

function usage() {
  echo "Usage: $0 <name> [options]"
}

# Error handling omitted (for now)

if [[ "$1" == -h ]]; then
  usage
  exit 0
fi

name="$1"
echo "Hello, ${name}!"

Вот что получится при запуске:

$ ./greeting -h
Usage: ./greeting [options]
$ bash greeting -h
Usage: greeting [options]
$ ./greeting "Ryan"
Hello, Ryan!

$#: число аргументов

Кроме позиционных параметров в Bash есть еще и специальные. Переменная $# хранит количество аргументов, переданных через командную строку. Это очень пригождается в обработке ошибок. Что произойдет, если наш скрипт не получит нужных ему аргументов? Давайте обновим скрипт из предыдущего примера и добавим обработку ошибок.

#!/usr/bin/env bash

function usage() {
  echo "Usage: $0 <name> [options]"
}

### Hot new error handling!
# We expect one argument.  Otherwise tell the user how to
# call your script.
if [[ "$#" -ne 1 ]]; then
  usage
  exit 1
fi

if [[ "$1" == -h ]]; then
  usage
  exit 0
fi

name="$1"
echo "Hello, ${name}!"

$?: последний код возврата

Лично я нечасто пользуюсь этим специальным параметром в скриптах, зато интенсивно использую его в командной строке. Многие команды, когда их выполнение проваливается, не выводят никаких сообщений. Они просто ничего не делают. Как же вам узнать, успешно ли отработала команда? Можно вывести значение переменной $?, в которой сохраняется код возврата последней запускавшейся команды.

$ ls
test.txt	code	strudel.py
$ echo $?
0
$ ls lamedir
ls: cannot access 'lamedir': No such file or directory
$ echo $?
2

Вот пример использования в скрипте:

#!/usr/bin/env bash

dirname="$1"

mkdir "$dirname"  # This will fail if the directory exists already

if [[ "$?" -ne 0 ]]; then
  # If the directory is already created, that's fine
  # just print out a message to alert the user
  echo "Directory '$dirname' already exists.  Not making a new one."
fi

$@ and $*: все аргументы

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

Если вы НЕ берете эти переменные в кавычки, они делают одно и то же: вставляют в указанное место все переданные в скрипт аргументы.

#!/usr/bin/env bash

echo "===================="
echo "This is dollar star."
echo "===================="
for arg in $*; do
  echo "$arg"
done

echo "===================="
echo "This is dollar at."
echo "===================="
for arg in $@; do
	echo "$arg"
done

При запуске получим следующее:

$ ./arg_printer abba dabba "dooby doo"
====================
This is dollar star.
====================
abba
dabba
dooby
doo
====================
This is dollar at.
====================
abba
dabba
dooby
doo

Обратите внимание на аргумент «dooby doo». Он был взят в кавычки при передаче, но в результате разбился по пробелу на два разных аргумента. Порой это именно то, что нужно, но очень часто — нет.

Переходим к самому интересному: возьмем переменные в кавычки.

Если взять в кавычки $*, в выводе вы получите все аргументы в одной строке. Аргументы будут разделены пробелами и фактически станут одним аргументом. Причем это не зависит от того, были ли они заключены в кавычки при вводе.

Примечание. На самом деле аргументы разделяются $IFS («внутренним разделителем полей»). Обычно это пробел, но стоит знать, что так бывает не всегда.

#!/usr/bin/env bash

echo "===================="
echo "This is quoted dollar star."
echo "===================="
for arg in "$*"; do
  echo "$arg"
done

Запуск:

$ ./arg_printer abba dabba "dooby doo"
====================
This is quoted dollar star.
====================
abba dabba dooby doo

Видите? Один аргумент! Хотите самостоятельно реализовать echo?

#!/usr/bin/env bash

printf '%s\n' "$*"
$ ./my_echo hello my name is Ryan
hello my name is Ryan

Ловко, да?

А вот когда вы берете в кавычки $@, Bash выводит все аргументы так, как они были переданы изначально. Это, на мой взгляд, самый полезный функционал, потому что позволяет передавать все аргументы подкомандам, сохраняя при этом пробелы и кавычки и не позволяя автоматическому разделению строк Bash все испортить.

#!/usr/bin/env bash

echo "===================="
echo "This is quoted dollar at."
echo "===================="
for arg in "$@"; do
  echo "$arg"
done
$ ./arg_printer abba dabba "dooby doo"
====================
This is quoted dollar at.
====================
abba
dabba
dooby doo

Вы часто увидите это в скриптах, содержащих множество функций. Традиционно, если у вас много функций, вы делаете последнюю функцию в скрипте функцией main. Она будет обрабатывать все аргументы и содержать организационную логику скрипта. А для запуска функции main обычно последней строчкой скрипта идет main "$@". Вот так:

#!/usr/bin/env bash

function usage() {
  echo "Usage: $0 <first> <second> [options]"
}

function bigger() {
  local first="$1"
  local second="$2"
  if [[ "$first" -gt "$second" ]]; then
    echo "$first"
  else
  	echo "$second"
  fi
}

function main() {
  if [[ "$#" -ne 2 ]]; then
  	usage
  	exit 1
  fi

  local first="$1"
  local second="$2"
  bigger "$first" "$second"
}

main "$@"

Итоги

Надеюсь, теперь вы начинаете понимать силу кастомизации. Используя скрипты Bash, вы можете автоматизировать выполнение многих задач. А благодаря возможности передачи аргументов при вызове скрипта вы можете автоматически выполнять даже те задачи, логика которых зависит от ситуации!