Начальные точки

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

Файлы и каталоги

Копирование файлов и каталогов

Первый сценарий демонстрирует, как скопировать папку, полную файлов и папок к различному использованию расположения cp.

Перечисление d-1  , Копирующее папку рекурсивно

cp -R -p folder_to_copy destination_directory

Следующий сценарий показывает, как скопировать дерево файлов и папок, сохранив исходное использование структуры каталогов tar. Например, это приводит к destination/file1, destination/dir2/file2, и т.д.

Перечисление d-2  , Копирующее многократные файлы и каталоги к другому расположению, сохраняя структуру каталогов

tar -czf - file1 dir2/file2 dir3/file3 | \
    { cd /destination ; tar -xzf - ; }

Следующие два сценария показывают, как скопировать все деревья файлов от одного сервера до другого надежно использование tar и ssh.

Перечисление d-3  , Копирующее дерево файлов и папок с текущего каталога на удаленный компьютер

# Copies directory_or_file_name on the local machine
# to /path/to/destination/directory_or_file_name on
# a remote machine.
 
tar -czf - directory_or_file_name | ssh username@hostname \
    "cd /path/to/destination; tar -xzf -"

Перечисление d-4  , Копирующее дерево файлов и папок от удаленного компьютера до текущего каталога

# Copies the directory called directory_name from
# /path/to/source/directory_name on a remote server
# to the current directory on the local machine.
 
ssh username@hostname "cd /path/to/source; \
    tar -czf - directory_name" | tar -xzf -

Следующий сценарий восстанавливается с неработающего tar копия. Обычно, Вы просто использовали бы rsync, но иногда Вам, вероятно, придется скопировать много файлов к или от запрещающего ISP rsync и устанавливает необоснованно низкое максимальное процессорное время для исполнимых программ, вызывая tar умирать неоднократно.

  Код перечисления d-5 для восстановления с усеченной копии tar

#!/bin/sh
 
USERNAME="remoteuser"
REMOTEHOST="remotehost.example.org"
SRCDIR="/path/to/testdir"
OUTDIR="/remote/path/here"
 
# Format is "path bytecount"
LOCALFORMATFLAG="-f"  # OS X
LOCALFORMAT="%N %z"   # OS X
REMOTEFORMATFLAG="-c" # Linux
REMOTEFORMAT="%n %s"  # Linux
 
OUTDIRQUOTED="$(echo "$OUTDIR" | sed 's/"/\\"/g')"
 
IFS="
"
 
BACKUPLIST=""
 
cd "$SRCDIR"
 
# Generate a list of files and their length in bytes on the local
# and local machines.
LOCALFILELIST="$(cd "$SRCDIR" ; find . -exec stat "$LOCALFORMATFLAG" \
    "$LOCALFORMAT" {}  \; | sort)"
REMOTEFILELIST="$(ssh $USERNAME@$REMOTEHOST "cd \"$OUTDIRQUOTED\" ; \
    find . -exec stat "$REMOTEFORMATFLAG" '$REMOTEFORMAT' {}  \; | sort")"
 
# echo "RFL: $REMOTEFILELIST"
 
# Loop until there are no more local files to check.
while true ; do
    LNFILES="$(echo "$LOCALFILELIST" | grep -c .)"
    LNFM1="$(expr "$LNFILES" '-' '1')"
    RNFILES="$(echo "$REMOTEFILELIST" | grep -c .)"
    RNFM1="$(expr "$RNFILES" '-' '1')"
 
    # echo "@TOP LNFM1: $LNFM1 RNFM1 $RNFM1"
 
    # If there are no more local files, break out of the outer loop.
    # Otherwise, pop the first filename from the list.
    if [ $LNFM1 -lt 0 ] ; then
        break;
    else
        LOCALLINE="$(echo "$LOCALFILELIST" | head -n 1)"
        LOCALFILE="$(echo "$LOCALLINE" | sed 's/ [0-9][0-9]*$//')"
        LOCALQUOTED="$(echo "$LOCALFILE" | sed 's/"/\\"/g')"
        LOCALLENGTH="$(echo "$LOCALLINE" | \sed 's/.* \([0-9][0-9]*\)$/\1/')"
        LOCALFILELIST="$(echo "$LOCALFILELIST" | tail -n $LNFM1)"
    fi
 
    # If there are no more remote files, every local file must
    # be added to the list of files to copy.
    # Otherwise, pop the first filename from the list.
    if [ $RNFM1 -lt 0 ] ; then
        REMOTELINE=""
        REMOTEFILE=""
        REMOTELENGTH=0
        REMOTEFILELIST=""
    else
        REMOTELINE="$(echo "$REMOTEFILELIST" | head -n 1)"
        REMOTEFILE="$(echo "$REMOTELINE" | sed 's/ [0-9][0-9]*$//')"
        REMOTELENGTH="$(echo "$REMOTELINE" | sed 's/.* \([0-9][0-9]*\)$/\1/')"
        REMOTEFILELIST="$(echo "$REMOTEFILELIST" | tail -n $RNFM1)"
    fi
 
    # echo "OLOOP LOCALFILE: $LOCALFILE REMOTEFILE: $REMOTEFILE"
    # echo "LOCALFILELIST: $LOCALFILELIST"
    # echo "REMOTEFILELIST: $REMOTEFILELIST"
 
    # If the filenames do not match, then the local file does
    # not exist on the remote server (because the lists are sorted).
    if [ "$LOCALFILE" != "$REMOTEFILE" ] ; then
 
        # Until they do match, keep adding files to the list of stuff to copy.
        while [ "$LOCALFILE" != "$REMOTEFILE" -a "$LOCALFILE" != "" ] ; do
            # echo "NOMATCHLOOP LOCALFILE: $LOCALFILE REMOTEFILE: $REMOTEFILE"
 
            # echo "ADDED \"$LOCALQUOTED\" TO BACKUP LIST"
 
            BACKUPLIST="$BACKUPLIST \"$LOCALQUOTED\""
 
            # If it is a directory, adding the directory to the archive
            # adds everything in it, so skip everything in it.
            if [ -d "$LOCALFILE" ] ; then
                # echo "ISDIR"
 
                DIRLOOP=1
                LList2="$LOCALFILELIST"
 
                # Loop until we run out of files or the names do not match.
                while [ $DIRLOOP = 1 ] ; do
                    LOCALFILE="$(echo "$LOCALFILE" | sed 's/\/$//')"
                    LOCALQUOTED="$(echo "$LOCALFILE" | sed 's/"/\\"/g')"
 
                    LNFILES2="$(echo "$LList2" | grep -c .)"
                    LNFM1_2="$(expr "$LNFILES2" '-' '1')"
 
                    # echo "LList2: $LList2"
                    if [ $LNFM1_2 -lt 0 ] ; then
                        # We ran out of files, so stop looking for files in
                        # the directory.
 
                        LLine2=""
                        LF2=""
                        LLen2=0
                        LList2=""
                        DIRLOOP=0
                    else
                        # Grab the next file in the list.
                        LLine2="$(echo "$LList2" | head -n 1)"
                        LF2="$(echo "$LLine2" | sed 's/ [0-9][0-9]*$//')"
                        LLen2="$(echo "$LLine2" | \
                            sed 's/.* \([0-9][0-9]*\)$/\1/')"
                        LList2="$(echo "$LList2" | tail -n $LNFM1_2)"
 
                        # echo "INDIRLOOP: FILE IS $LF2"
 
                        # Repeatedly strip off the last part of the path
                        # until it matches or the path is empty.
                        INDIR="NO"
                        while [ "$LF2" != "" -a "$LF2" != "." ] ; do
                            # echo "LF2: \"$LF2\""
                            LF2="$(dirname "$LF2" | sed 's/\/$//')";
                            if [ "$LF2" = "$LOCALFILE" ] ; then
                                # It matches.  The file is in the directory.
                                INDIR="YES"; LF2="";
                            fi
                        done
                        if [ $INDIR = "YES" ] ; then
                            # Because this file is in the directory, commit
                            # the changes to the local file list (thus
                            # removing this file from the list).
 
                            # echo "INDIR"
                            LOCALFILELIST="$LList2"
                        else
                            # This file is not in the directory.  Don't take it
                            # off the list, and stop looking for files in the
                            # directory.
 
                            # echo "NOTINDIR"
                            DIRLOOP=0
                        fi
                    fi
                done
 
                # Recount the number of files in the local list because it may
                # have changed significantly.
                LNFILES="$(echo "$LOCALFILELIST" | grep -c .)"
                LNFM1="$(expr "$LNFILES" '-' '1')"
 
            else
                # It is not a directory.  Pop the file from the list.
 
                # echo "@BOTTOM LOCALFILELIST: $LOCALFILELIST"
 
                # Recount the number of files in the local list.
                LNFILES="$(echo "$LOCALFILELIST" | grep -c .)"
                LNFM1="$(expr "$LNFILES" '-' '1')"
 
                # echo "@BOTTOM LNFM1: $LNFM1 RNFM1 $RNFM1"
 
                # Grab the next file.  This is the middle loop iterator
                # testing to see if the filename matches.
                if [ $LNFM1 -lt 0 ] ; then
                    LOCALLINE=""
                    LOCALFILE=""
                    LOCALQUOTED=""
                    LOCALLENGTH=0
                    LOCALFILELIST=""
                else
                    LOCALLINE="$(echo "$LOCALFILELIST" | head -n 1)"
                    LOCALFILE="$(echo "$LOCALLINE" | sed 's/ [0-9][0-9]*$//')"
                    LOCALQUOTED="$(echo "$LOCALFILE" | sed 's/"/\\"/g')"
                    LOCALLENGTH="$(echo "$LOCALLINE" | \
                        sed 's/.* \([0-9][0-9]*\)$/\1/')"
                    LOCALFILELIST="$(echo "$LOCALFILELIST" | tail -n $LNFM1)"
                fi
 
            fi
        done
    fi
 
    # When the script reaches this point,
    if [ "$LOCALFILE" = "$REMOTEFILE" -a "$LOCALFILE" != "" \
            -a $LOCALLENGTH != $REMOTELENGTH ] ; then
        if [ ! -d "$LOCALFILE" ] ; then
            # echo "ADDED \"$LOCALQUOTED\" TO BACKUP LIST"
            BACKUPLIST="$BACKUPLIST \"$LOCALQUOTED\""
        fi
    fi
done
 
echo "BACKUPLIST $BACKUPLIST"
 
if [ "$BACKUPLIST" != "" ] ; then
        eval tar -czf - $BACKUPLIST  | ssh $USERNAME@$REMOTEHOST \
                "cd \"$OUTDIRQUOTED\" ; tar -xzf -"
fi

Переименование файлов

Следующий пример показывает, как стандартизировать случай расширения файла на файлах образа.

find photo_directory -iname '*.jpg' -exec \
    mv {} `echo {} | sed 's/\.[jJ][pP][gG]$/.jpg/'` \;

Преобразование окончаний строки файла

Перечисление 10-1 и Перечисление 10-2 показывают, как преобразовать между форматами окончания строки, используемыми для текстовых файлов на различных платформах.

Обработка изображения

В Усовершенствованных Методах Перечисление 11-13 показывает, как изменить размеры использования изображения osascript.

В дополнение к osascript интерфейс, OS X включает sips команда, обеспечивающая прямой интерфейс оболочки для некоторых функций обработки изображений в OS X.

Следующий отрывок показывает, как использовать sips масштабировать изображение максимум к 250 пикселей горизонтально или вертикально и преобразовать изображение в формат JPEG.

sips -s format jpeg --resampleHeightWidthMax 250 myphoto.tif --out mythumb.jpg

Можно также объединиться sips с exiftool (доступный от http://www .sno.phy.queensu.ca / ~ phil/exiftool/) для еще большего питания и управления. Следующее использование сценария sips и exiftool автоматически поворачивать фотографию на основе закодированной информации об ориентации, и позволяет Вам указывать смещение (в инкрементах на 90 градусов) для корректировки вращения далее.

Перечисление d-6  , Вращающееся изображение с помощью глотков

#!/bin/sh
 
# Adjust paths as needed
EXIFTOOL=/usr/local/bin/exiftool
SIPS=/usr/bin/sips
 
INPUTFILE="$1"
OUTPUTFILE="$2"
OFFSET="$3"
 
# If the user doesn't specify an offset, assume zero.
if [ "$OFFSET" = "" ] ; then
    OFFSET=0
fi
 
# Use exiftool to read the EXIF orientation tag as a raw numeric value.
ORIENTATION="$($EXIFTOOL -b -Orientation $INPUTFILE)"
 
# If no orientation tag is found, assume no rotation is needed.
if [ "$ORIENTATION" = "" ] ; then
    ORIENTATION=1
fi
 
# This table determines the rotation (in 90 degree increments)
# based on the EXIF orientation tag and determines whether a
# coordinate transformation is needed.
case $ORIENTATION in
    (1)    ROT=0; FLIP=0;; # No rotation or flip needed.
    (2)    ROT=0; FLIP=1;; # Flip horizontal.
    (3)    ROT=2; FLIP=0;; # Rotate 180, no flip.
    (4)    ROT=2; FLIP=1;; # Rotate 180, flip.
    (5)    ROT=3; FLIP=1;; # Rotate 270, flip.
    (6)    ROT=1; FLIP=0;; # Rotate 90, no flip.
    (7)    ROT=1; FLIP=1;; # Rotate 90, flip.
    (8)    ROT=3; FLIP=0;; # Rotate 270, no flip.
    (*)    echo "BAD ORIENTATION $ORIENTATION" ; exit -1;;
esac
 
# Calculate the number of degrees to rotate the image
# based on the above table and the user-entered adjustment.
DEGREES="$(expr 90 '*' '(' $OFFSET '+' $ROT ')')"
 
# Generate the additional flags for sips if flipping is required.
FLIPSTR=""
if [ $FLIP = 1 ] ; then
    FLIPSTR="--flip horizontal"
else
    FLIPSTR=""
fi
 
# Perform the transformation.
$SIPS $FLIPSTR --rotate $DEGREES $INPUTFILE --out $OUTPUTFILE
 
# Delete the orientation keys so that sips and other tools
# won't get confused when doing auto-rotation.
$EXIFTOOL -Orientation= $OUTPUTFILE

Сети

Используя SIGSTOP и SIGCONT для управления долгосрочными демонами

Этот прием предотвращает Ftp-серверы на соединениях DSL от безнадежного засорения восходящего канала при помощи killall команда. Это также захватывает Ctrl-C и другие вероятные сигналы так, чтобы, если Вы убегаете из сценария, процессы FTP были перезапущены правильно.

Перечисление d-7  , Замедляющее Ftp-сервер

#!/bin/sh
 
SECONDS_TO_RUN=5
SECONDS_TO_PAUSE=20
 
handler() {
    killall -CONT ftpd
    exit 0
}
 
trap handler SIGHUP SIGTERM SIGQUIT SIGINT
# This must be run as root or the ftp user.
while true ; do
    killall -STOP ftpd
    sleep $SECONDS_TO_PAUSE
    killall -CONT ftpd
    sleep $SECONDS_TO_RUN
done

Основанный на Shell веб-сервер

Раздел Networking With Shell Scripts в Усовершенствованных Методах описал, как записать простому демону, использующему netcat. Возможно записать удивительно сложным демонам, использующим этот метод.

Первый шаг в демоне HTTP анализирует начальный запрос. Для простых запросов GET без строк запроса это - тривиальная фея. Следующий отрывок проводит строку запроса как параметр и устанавливает глобальные переменные, содержащие тип запроса, URL и версию HTTP.

parseRequest()
{
    local REQUEST="$(echo "$1" | tr -d '\r')"
 
    TYPE="$(echo "$REQUEST" | cut -f 1 -d ' ')"
    URL="$(echo "$REQUEST" | cut -f 2 -d ' ')"
    VERSION="$(echo "$REQUEST" | cut -f 3 -d ' ')"
 
    echo "GOT REQUEST: $REQUEST" 1>&2
}

Прежде чем можно будет фактически интерпретировать запрос, однако, необходимо отколоться строка запроса, если это там. Например, URL http://example.org/foo.cgi?bar содержит часть узла (example.org), часть пути (/foo.cgi), и строка запроса (bar). Этот код не откалывается часть узла, потому что это отправляется отдельно от строки Запроса HTTP в HTTP/1.1 и опущено полностью в HTTP/1.0.

splitURL()
{
        URL="$1"
        PATHPART="$(echo "$URL" | sed 's/?.*$//g')"
        local PATHLEN="$(strlen "$PATHPART")";
        local CUTPOS="$(expr "$PATHLEN" "+" "2")"
 
        PARMPART="$(echo "$URL" | cut -c "$CUTPOS-")"
}

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

parseHeaders()
{
        local FD="$1"
        local TREENAME="$2"
        local HEADERLINE
 
        if [ "$TREENAME" = "" ] ; then
                TREENAME="HEADERTREE"
        fi
 
        # Creates a new tree head object with the specified name.
        newTree "$TREENAME"
        eval $TREENAME=\"\$\(getLastNodeName\)\"
 
        # echo "TN: $TREENAME" 1>&2
 
        # Reads headers from the specified file descriptor until
        # it gets a blank line, pasing each one to a parser..
        while true ; do
                eval read -u$FD HEADERLINE
                HEADERLINE="$(echo "$HEADERLINE" | tr -d '\r')"
                # echo "GOT HEADER LINE: \"$HEADERLINE\"" 1>&2
 
                if [ "$HEADERLINE" = "" ] ; then
                        # End of headers reached.
                        # echo "End of headers" 1>&2
                        break;
                fi
 
                addHeaderLine "$HEADERLINE" "$TREENAME"
        done
        LAST_TREE_NODE_INSERTED="$TREENAME"
}

Следующая часть, addHeaderLine, тривиально анализирует строку заголовка путем разделения строки на первом двоеточии (:) символ и снимающий изоляцию с любого ведущего пробела после него. Затем это вызывает другую функцию для добавления его к двоичному дереву.

addHeaderLine()
{
        local HEADERLINE="$1"
        local TREE="$2"
 
        local FIELDNAME="$(echo "$HEADERLINE" | cut -f 1 -d ':')"
        local FIELDVALUE="$(echo "$HEADERLINE" | cut -f 2- -d ':' | \
            sed 's/^[[:space:]]//g')"
 
        addHeader "$FIELDNAME" "$FIELDVALUE" "$TREE"
}

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

addHeader()
{
        local FIELDNAME="$1"
        local FIELDVALUE="$2"
        local TREE="$3"
 
        # echo "Inserting $FIELDNAME with value $FIELDVALUE into $TREE" 1>&2
        insertKey "$TREE" "$FIELDNAME"
        NODE="$(getLastNodeName)"
        setTreeField "$NODE" "Contents" "$FIELDVALUE"
}

Все, что остается, должно связать код и фактически обработать запросы. Для наблюдения кода в действии загрузите Сопутствующий архив zip Файлов, связанный с этим документом. (См. оглавление в версии HTML этого документа в developer.apple.com.)

В Сопутствующем архиве Файлов можно найти выборку в scripts/BB_Starting_Points/networking/shttpd.

Этот сценарий требует измененной версии версии OS X netcat, обеспечивающего расширенную функциональность и возможности восстановления после ошибки вне того, какой стандарт netcat версии обеспечивают. Make-файл (в Сопутствующем архиве Файлов) загрузки, сборки и установки эта измененная версия netcat. Патч должен также быть прост примениться к версии OpenBSD netcat.

Текстовое манипулирование

Управление данными

Работа с деревьями двоичного поиска

Иногда, полезно сохранить массив словарей пар ключ/значение и быть в состоянии быстро перерыть тот массив. Перечисление d-9 обеспечивает такую функциональность в форме двоичного дерева.

Эта библиотека двоичного дерева содержит много ключевых функций:

  • Общие древовидные функции:

    newTree(optional_tree_name)

    Создает новое двоичное дерево.

    deleteTree(tree_name)

    Удаляет двоичное дерево, освобождая ресурсы, связанные с ним.

    iterateTree(tree_name, callback, call_on_root=0)

    Выполняет итерации через поддерево, вызывая функцию для каждого узла.

    mergeTrees(source_tree_name, dest_tree_name)

    Копии все ключи в одном дереве в другого. В случае коллизии для данного ключа имеют приоритет новые значения.

  • Функции вставки:

    insertKey(tree_name, key)

    Вставляет новый ключ в двоичное дерево с помощью сравнений строк.

    insertKeyNumeric(tree_name, key)

    Вставляет новый ключ в двоичное дерево с помощью числовых сравнений.

    getLastNodeName()

    Получает последний вставленный узел.

  • Функции узла:

    treeKey(node_name)

    Получает ключ, связанный с объектом узла.

    treeField(node_name, field_name)

    Получает значение поля для узла в дереве.

    setTreeField(node_name, field_name, new_value)

    Устанавливает значение поля для узла в дереве.

  • Поисковые функции:

    treeSearch(tree_name, key)

    Ищет двоичное дерево данное ключевое использование сравнения строк.

    treeSearchNumeric(tree_name, key)

    Ищет двоичное дерево данный ключевые использующие числовые сравнения.

Следующий код демонстрирует, как пользоваться этой библиотекой двоичного дерева:

  Пример Двоичного дерева перечисления d-8

# Tell the binary tree library to not run its tests.
DISABLE_TESTS=true
. binary_tree.sh
 
# Create a new binary tree and obtain its name.
newTree
TESTTREE="$(getLastNodeName)"
 
# Insert three nodes into the tree
# with keys 1, 3, and 7.
insertKeyNumeric "$TESTTREE" 3
insertKeyNumeric "$TESTTREE" 7
insertKeyNumeric "$TESTTREE" 1
 
# Add an attribute to the last node inserted (1)
ONENODE="$(getLastNodeName)"
setTreeField "$ONENODE" "MyFieldName" "42"
 
# Takes a node and prints the key value and
# the value of MyFieldName
echokeyandmyfieldname()
{
    echo "$(treeKey "$1") -> $(treeField "$1" "MyFieldName")"
}
 
# Iterate the tree in key order and call
# echokeyandmyfieldname on each node
iterateTree "$TESTTREE" "echokeyandmyfieldname"

Без дальнейшего введения вот библиотека кода двоичного дерева. (Версия в сопутствующем архиве файлов также включает некоторый тестовый код.)

Перечисление d-9  binary_tree.sh от shttpd

#!/bin/sh
 
# /*!
#     @header
#         A binary tree algorithm written in a shell script.  The main
#         functions of interest are {@link newTree}, {@link deleteTree},
#         {@link insertKey}, {@link insertKeyNumeric}, {@link treeSearch},
#         {@link treeSearchNumeric}, {@link iterateTree}, and
#         {@link mergeTrees}.
#
#         This is a minimal binary tree implementation that does not support
#         removing existing values from the tree once inserted.  However, such
#         functionality can be trivially retrofitted on top by adding or
#         clearing a "deleted" attribute on nodes using {@link setTreeField} if
#         desired.
#
#         To use this shell script, source it after setting DISABLE_TESTS to
#         "true".  To run tests, execute the script directly.
#  */
 
# /*! @group Global Variables
#         Variables used internally.  No user-serviceable parts inside.
#  */
 
# /*!
#     @abstract The starting object ID.  This is an internal counter.
#  */
OID=0
 
# /*!
#     @abstract A newline character.
#  */
NEWLINE="
"
 
# /*!
#     @abstract
#         Field separator.  Do not change.
#  */
IFS="$NEWLINE"
 
# /*! @group Node Functions
#         Functions that operate on a single node in the tree.
#  */
 
# /*!
#     @abstract Retrieves the key associated with a node object.
#     @result
#         Returns the key via <code>stdout</code>.
#     @param NODE
#         The node object.
#  */
treeKey()
{
        local NODE="$1"
 
        eval echo "\$$NODE"_KEY
}
 
# /*!
#     @abstract
#         Retrieves the left subtree for a node in the tree.
#     @result
#         Returns the node name of the left subtree via <code>stdout</code>.
#     @discussion
#         This is mainly an internal function, though you can use
#         it for debugging purposes.
#     @param NODE
#         The node object.
#  */
treeLeft()
{
        local NODE="$1"
 
        eval echo "\$$NODE"_LEFT
}
 
# /*!
#     @abstract
#         Sets the left subtree for a node in the tree.
#     @discussion
#         This is an internal function.  Do not call it directly.  Use
#         {@link insertKey} or {@link insertKeyNumeric} instead.
#     @param NODE
#         The node object.
#     @param VAL
#         The new left value.
#  */
setTreeLeft()
{
        local NODE="$1"
        local VAL="$2"
 
        eval "$NODE"_LEFT=\"$VAL\"
}
 
# /*!
#     @abstract
#         Retrieves the right subtree for a node in the tree.
#     @result
#         Returns the node name of the right subtree via <code>stdout</code>.
#     @discussion
#         This is mainly an internal function, though you can use
#         it for debugging purposes.
#     @param NODE
#         The node object.
#  */
treeRight()
{
        local NODE="$1"
 
        eval echo "\$$NODE"_RIGHT
}
 
# /*!
#     @abstract
#         Sets the right subtree for a node in the tree.
#     @discussion
#         This is an internal function.  Do not call it directly.  Use
#         {@link insertKey} or {@link insertKeyNumeric} instead.
#     @param NODE
#         The node object.
#     @param VAL
#         The new right value.
#  */
setTreeRight()
{
        local NODE="$1"
        local VAL="$2"
 
        eval "$NODE"_RIGHT=\"$VAL\"
}
 
# /*!
#     @abstract
#         Retrieves a field value for a node in the tree.
#     @result
#         Returns the requested field value via <code>stdout</code> or
#         an empty string.
#     @seealso setTreeField
#     @param NODE
#         The node object.
#     @param FIELDNAME
#         The field name.
#  */
treeField()
{
        local NODE="$1"
        local FIELDNAME="$2"
 
        eval echo "\$$NODE"_DATAFIELD_"$FIELDNAME"
}
 
# /*!
#     @abstract
#         Sets a field value for a node in the tree.
#     @discussion
#         This function allows you to store arbitrary attributes in a tree node.
#         If a value already exists for the specified field name, the value is
#         overwritten.
#     @param NODE
#         The node object.
#     @param FIELDNAME
#         The field name.
#     @param VAL
#         The new field value.
#  */
setTreeField()
{
        local NODE="$1"
        local FIELDNAME="$2"
        local VAL="$3"
 
        eval "$NODE"_DATAFIELD_"$FIELDNAME"=\"$VAL\"
        local DATAFIELDS="$(eval echo "\$$NODE"_DATAFIELDS)"
        eval "$NODE"_DATAFIELDS="\"$DATAFIELDS$NEWLINE$FIELDNAME\""
}
 
 
# /*! @group General Tree Functions
#         Operations that create, delete, iterate, and merge trees.
#  */
 
# /*!
#     @abstract
#         Iterates through a subtree, calling a function for each node.
#     @discussion
#         For each node in the tree (in sorted order), the function
#         specified by ACTION is called with a single parameter
#         containing the node name of the node being traversed.
#     @param TREE
#         The tree to traverse.
#     @param ACTION
#         The function to call on each node.
#     @param CALLONROOT
#         Set to 1 if you want to also call ACTION on the (bogus) root node.
#         This is usually only set for debug printing purposes.
#  */
iterateTree()
{
        local TREE="$1"
        local ACTION="$2"
        local CALLONROOT="$3"
 
        # echo "NAME IS $TREE"
 
        if [ "$CALLONROOT" = "1" ] ; then
                eval "$ACTION" "$TREE"
        fi
 
        iterateSubtree "$(treeLeft "$TREE")" "$ACTION"
}
 
# /*!
#     @abstract
#         Copies all of the keys in one tree into another.
#     @discussion
#         For each key in TREE_SRC, an equivalent key is
#         inserted in TREE_DST, including any field values
#         associated with it.  In the event of a collision
#         for a given key, the resulting set of field values
#         for that key is the union of the two sets of field
#         values, with the new values from TREE_SRC taking
#         precedence.
#     @param TREE_SRC
#         The source tree to copy.
#     @param TREE_DST
#         The destination tree into which the source tree is copied.
#  */
mergeTrees()
{
        local TREE_SRC="$1"
        local TREE_DST="$2"
 
        # echo "Here SRC: $TREE_SRC (left is $(treeLeft "$TREE_SRC"))" 1>&2
        # echo "     DST: $TREE_DST" 1>&2
 
        iterateSubtree "$(treeLeft "$TREE_SRC")" reinsert
}
 
# /*!
#     @abstract
#         Deletes a binary tree.
#     @param TREE
#         The name of the tree to delete.
#  */
deleteTree()
{
        local TREE="$1"
 
        if [ "$TREE" = "" ] ; then
                return;
        fi
 
        deleteTree "$(treeLeft "$TREE")"
        deleteTree "$(treeRight "$TREE")"
        deleteNode "$TREE"
}
 
# /*!
#     @abstract
#         Creates a new binary tree.
#     @result
#         Obtain the name of the tree using {@link getLastNodeName}.
#     @param TREE
#         The name of the tree to create.
#  */
newTree()
{
        local TREE="$1"
 
        newTreeNode "" "" "" "$TREE"
}
 
# /*! @group Search Functions
#         Functions used for searching for a key in a tree.  Be sure to
#         choose whether you want to use numerical or string key comparisons
#         for the search and choose the appropriate function accordingly.
#         The comparison type usde for searching must match the comparison
#         type used during insertion or the results are undefined.
#  */
 
# /*!
#     @abstract
#         Searches a binary tree for a given key.
#     @discussion
#         This tree search uses string comparisons.  You must use
#         {@link insertKey} with this function (and not
#         {@link insertKeyNumeric}.  For numeric searches, use
#         {@link treeSearchNumeric}.
#     @result
#         Returns the node name of the matching node through <code>stdout</code>
#         if found or an empty string otherwise.
#     @param TREE
#         The tree to search.
#     @param KEY
#         The key to search for.
#  */
treeSearch()
{
        local TREE="$1"
        local KEY="$2"
 
        subtreeSearch "$(treeLeft "$TREE")" "$KEY"
}
 
# /*!
#     @abstract
#         Searches a binary tree for a given key.
#     @result
#         Returns the node name of the matching node through <code>stdout</code>
#         if found or an empty string otherwise.
#     @discussion
#         This tree search uses numeric comparisons.  You must use
#         {@link insertKeyNumeric} with this function (and not
#         {@link insertKey}.  For string searches, use {@link treeSearch}.
#     @param TREE
#         The tree to search.
#     @param KEY
#         The key to search for.
#  */
treeSearchNumeric()
{
        local TREE="$1"
        local KEY="$2"
 
        subtreeSearchNumeric "$(treeLeft "$TREE")" "$KEY"
}
 
# /*! @group Insertion Functions
#         Functions used for inserting a key into a tree.  Be sure to
#         choose whether you want to use numerical or string key comparisons
#         during insertion and choose the appropriate function accordingly.
#
#         After inserting, you can use {@link getLastNodeName} to get the
#         node name of the resulting node if desired.
#  */
 
# /*!
#     @abstract
#         Retrieves the last node inserted.
#     @result
#         Returns the node name of the last node inserted via
#         <code>stdout</code>.
#     @discussion
#         After creating a new node with {@link insertKey} or a
#         new tree with {@link newTree}, call this to obtain its
#         note ID.
#  */
getLastNodeName()
{
        echo "$LAST_TREE_NODE_INSERTED"
}
 
# /*!
#     @abstract
#         Inserts a new key into a binary tree.
#     @discussion
#         If a node already exists with this value, the
#         existing node is returned.
#
#         This tree insertion uses string comparisons.  You must use
#         {@link treeSearch} with this function (and not
#         {@link treeSearchNumeric}.  For numeric searches, use
#         {@link insertKeyNumeric}.
#     @result
#         Obtain the node name of the newly created node using
#         {@link getLastNodeName}.
#     @param TREE
#         The name of the binary tree.
#     @param KEY
#         The key to insert.
#  */
insertKey()
{
        local TREE="$1"
        local KEY="$2"
 
        local LASTTREE="$TREE"
        local DIRECTION="LEFT"
 
        while [ "$TREE" != "" -a "$LASTTREE" != "" ] ; do
                if [ $DIRECTION = "LEFT" ] ; then
                        TREE="$(treeLeft "$TREE")"
                else
                        TREE="$(treeRight "$TREE")"
                fi
                local TREEKEY="$(treeKey "$TREE")"
 
                if [ "$TREE" != "" ] ; then
                        if [ "$KEY" \< "$TREEKEY" ] ; then
                                DIRECTION="LEFT"
                                LASTTREE="$TREE"
                        elif [ "$KEY" \> "$TREEKEY" ] ; then
                                DIRECTION="RIGHT"
                                LASTTREE="$TREE"
                        else
                                # Matching node already exists.  Return its name.
                                LAST_TREE_NODE_INSERTED="$NODE"
                                return
                        fi
                fi
        done
        newTreeNode "" "" "$KEY"
        local NODE="$(getLastNodeName)"
 
        if [ $DIRECTION = "LEFT" ] ; then
                setTreeLeft "$LASTTREE" "$NODE"
        else
                setTreeRight "$LASTTREE" "$NODE"
        fi
}
 
# /*!
#     @abstract
#         Inserts a new key into a binary tree.
#     @discussion
#         If a node already exists with this value, the
#         existing node is returned.
#
#         This tree insertion uses string comparisons.  You must use
#         {@link treeSearch} with this function (and not
#         {@link treeSearchNumeric}.  For numeric searches, use
#         {@link insertKeyNumeric}.
#     @result
#         Obtain the node name of the newly created node using
#         {@link getLastNodeName}.
#     @param TREE
#         The name of the binary tree.
#     @param KEY
#         The key to insert.
#  */
insertKeyNumeric()
{
        local TREE="$1"
        local KEY="$2"
 
        # echo "IN INSNUM"
 
        local LASTTREE="$TREE"
        local DIRECTION="LEFT"
 
        while [ "$TREE" != "" -a "$LASTTREE" != "" ] ; do
                if [ $DIRECTION = "LEFT" ] ; then
                        TREE="$(treeLeft "$TREE")"
                else
                        TREE="$(treeRight "$TREE")"
                fi
                local TREEKEY="$(treeKey "$TREE")"
 
                if [ "$TREE" != "" ] ; then
                        if [ "$KEY" -lt "$TREEKEY" ] ; then
                                DIRECTION="LEFT"
                                LASTTREE="$TREE"
                        elif [ "$KEY" -gt "$TREEKEY" ] ; then
                                DIRECTION="RIGHT"
                                LASTTREE="$TREE"
                        else
                                # Matching node already exists.  Return its name.
                                LAST_TREE_NODE_INSERTED="$NODE"
                                return
                        fi
                fi
        done
        newTreeNode "" "" "$KEY"
        local NODE="$(getLastNodeName)"
 
        if [ $DIRECTION = "LEFT" ] ; then
                setTreeLeft "$LASTTREE" "$NODE"
        else
                setTreeRight "$LASTTREE" "$NODE"
        fi
}
 
 
# /*! @group Debug Functions
#         Functions that print debug information about binary trees,
#         tree nodes, and so on.
#  */
 
# /*!
#     @abstract
#         Prints a node structure for debugging purposes.
#     @param NODE
#         The node to print.
#  */
printNode()
{
        local NODE="$1"
 
        echo "NAME:  $NODE"
        echo "KEY:   $(treeKey "$NODE")"
        echo "LEFT:  $(treeLeft "$NODE")"
        echo "RIGHT: $(treeRight "$NODE")"
        echo "DATA:"
 
        local DATAFIELDS="$(eval echo "\$$NODE"_DATAFIELDS)"
        local FIELDNAME
        for FIELDNAME in $DATAFIELDS ; do
                # Skip the empty first field.
                if [ "$FIELDNAME" != "" ] ; then
                        eval echo "     $NODE""_DATAFIELD_$FIELDNAME"":" \
                                "\$$NODE""_DATAFIELD_$FIELDNAME"
                fi
        done
        echo "-=-=-=-=-=-=-=-=-=-=-=-"
}
 
# /*!
#     @abstract
#         Prints out the contents of a tree for debugging purposes.
#  */
printTree()
{
        local TREE="$1"
 
        # echo "NAME IS $TREE"
        iterateTree "$TREE" "printNode" 1
}
 
# /*!
#     @abstract
#         Prints a line of text in red letters.
#  */
echored()
{
        printf "\e[1;31m%s\e[0;30m\n" $@
}
 
# /*!
#     @abstract
#         Prints a line of text in green letters.
#  */
echogreen()
{
        printf "\e[1;32m%s\e[0;30m\n" $@
}
 
# /*!
#     @abstract
#         Prints a line of text in blue letters.
#  */
echoblue()
{
        printf "\e[1;34m%s\e[0;30m\n" $@
}
 
 
# /*! @group Internal Functions
#         No user-serviceable parts inside.  These functions are used
#         internally by the other functions and should generally not
#         be called from outside unless you really know what you are
#         doing.
#  */
 
# /*!
#     @abstract
#         Iterates through a subtree, calling a function for each node.
#     @discussion
#         Do not call this directly.  Call {@link iterateTree} instead.
#  */
iterateSubtree()
{
        local TREE="$1"
        local ACTION="$2"
 
        if [ "$TREE" = "" ] ; then
                return;
        fi
        # echo "IN IST: TREE $TREE" 1>&2
 
        iterateSubtree "$(treeLeft "$TREE")" "$ACTION"
        eval "$ACTION $TREE"
        iterateSubtree "$(treeRight "$TREE")" "$ACTION"
 
}
 
# /*!
#     @abstract
#         Internal helper function.
#     @discussion
#         This function is used by {@link mergeTrees} to take a node from
#         one tree and duplicte it in another.
#  */
reinsert()
{
        local NODE="$1"
 
        # echo "GOT NODE \"$NODE\"" 1>&2
 
        # echo "TREE_DST: $TREE_DST" 1>&2
 
        if [ "$NODE" = "" ] ; then
                return;
        fi
 
        local VAL="$(treeKey "$NODE")"
 
        if [ "$VAL" = "" ] ; then
                return;
        fi
 
        # local NEWNODE="$(treeSearch "$TREE_DST" "$VAL")"
        # echo "NN1: $NEWNODE"
 
        insertKey "$TREE_DST" "$VAL"
        local NEWNODE="$(getLastNodeName)"
 
        # print "NN: $NEWNODE" 1>&2
 
        local DATAFIELDS="$(eval echo "\$$NODE"_DATAFIELDS)"
        local FIELDNAME
 
        for FIELDNAME in $DATAFIELDS ; do
                # Skip the empty first field.
                if [ "$FIELDNAME" != "" ] ; then
                        # eval echo setting "$NEWNODE""_DATAFIELD_$FIELDNAME""=\"\$$NODE""_DATAFIELD_$FIELDNAME\"" 1>&2
                        eval "$NEWNODE""_DATAFIELD_$FIELDNAME""=\
                                \"\$$NODE""_DATAFIELD_$FIELDNAME\""
                fi
        done
 
        # printNode "$NODE"
}
 
# /*!
#     @abstract
#         Creates a new node in the tree.
#     @discussion
#         This is an internal function.  Do not call it directly.  Use
#         {@link insertKey} or {@link insertKeyNumeric} instead.
#     @param LEFT
#         The initial left value for the node (usually empty).
#     @param RIGHT
#         The initial right value for the node (usually empty).
#     @param KEY
#         The key for the new node.
#     @param TREE
#         The desired name for the node (usually empty).
#  */
newTreeNode()
{
        local LEFT="$1"
        local RIGHT="$2"
        local KEY="$3"
        local TREE="$4"
 
        if [ "$TREE" = "" ] ; then
                TREE="TREENODE_$OID"
                OID="$(expr "$OID" "+" "1")"
                # echo "$TREE"
        # else
                # echo "Using explicit name \"$TREE\"" 1>&2
        fi
 
        eval "$TREE"_LEFT=\"$LEFT\"
        eval "$TREE"_RIGHT=\"$RIGHT\"
        eval "$TREE"_KEY=\"$KEY\"
        LAST_TREE_NODE_INSERTED="$TREE"
}
 
 
# /*!
#     @abstract
#         Searches a binary tree for a given key.
#     @discussion
#         This is an internal function.  Do not call it directly.  Use
#         {@link treeSearch} instead.
#     @result
#         Returns the node name of the matching node through <code>stdout</code>
#         if found or an empty string otherwise.
#     @param TREE
#         The subtree to search.
#     @param KEY
#         The key to search for.
#  */
subtreeSearch()
{
        local TREE="$1"
        local KEY="$2"
 
        if [ "$TREE" = "" ] ; then
                return;
        fi
 
        local TREEKEY="$(treeKey "$TREE")"
 
        if [ "$KEY" \< "$TREEKEY" ] ; then
                subtreeSearch "$(treeLeft "$TREE")" "$KEY"
        elif [ "$KEY" \> "$TREEKEY" ] ; then
                subtreeSearch "$(treeRight "$TREE")" "$KEY"
        else
                echo $TREE
        fi
}
 
# /*!
#     @abstract
#         Searches a binary tree for a given key.
#     @discussion
#         This is an internal function.  Do not call it directly.  Use
#         {@link treeSearch} instead.
#     @result
#         Returns the node name of the matching node through <code>stdout</code>
#         if found or an empty string otherwise.
#     @param TREE
#         The subtree to search.
#     @param KEY
#         The key to search for.
#  */
subtreeSearchNumeric()
{
        local TREE="$1"
        local KEY="$2"
 
        if [ "$TREE" = "" ] ; then
                return;
        fi
 
        local TREEKEY="$(treeKey "$TREE")"
 
        if [ "$KEY" -lt "$TREEKEY" ] ; then
                subtreeSearchNumeric "$(treeLeft "$TREE")" "$KEY"
        elif [ "$KEY" -gt "$TREEKEY" ] ; then
                subtreeSearchNumeric "$(treeRight "$TREE")" "$KEY"
        else
                echo $TREE
        fi
}
 
# /*!
#     @abstract
#         Deletes a node in a tree.
#     @discussion
#         This algorithm does not support deleting arbitrry nodes.
#         This is an internal function that is used by {@link deleteTree}.
#     @param NODE
#         The node to delete.
#  */
deleteNode()
{
        local NODE="$1"
 
        local DATAFIELDS="$(eval echo "\$$NODE"_DATAFIELDS)"
 
        local FIELDNAME
        for FIELDNAME in $DATAFIELDS ; do
                # Skip the empty first field.
                if [ "$FIELDNAME" != "" ] ; then
                        eval unset "$NODE"_DATAFIELD_$FIELDNAME
                fi
        done
        eval unset "$NODE"_LEFT
        eval unset "$NODE"_RIGHT
}

Управление пользователями и групповое управление

OS X обеспечивает значительные инструменты GUI для управления пользователями и группами. Иногда, однако, Вы, возможно, должны сделать вещи твердый путь (из командной строки). Для случайного ручного дополнения можно вручную добавить пользователя или группу, использующую dscl (командная строка службы каталогов) инструмент. Однако, если регулярно необходимо добавлять пользователей, может быть выгодно написать сценарий задачи.

Листинги кода здесь (которые также включены в Сопутствующий архив Файлов) показывают, как создать нового пользователя и новую группу, включая выбор неиспользованного пользователя и группы IDs.

  Сценарий перечисления D-10 для добавления нового пользователя, использующего dscl (adduser.sh)

#!/bin/sh
 
# Usage:
#
# adduser [-a] <USERNAME> <LONGNAME> <PRIMARY_GID> [ <HOME_DIRECTORY> [ <UID> ]]
#
# -a: Make the user an admin user.
# USERNAME: The OS X "short name", e.g. jdoe
# LONGNAME: The OS X "real name", e.g. "John Doe"
# PRIMARY_GID: The primary group ID.
# HOME_DIRECTORY: The user's home directory.  Leave blank to use /Users/username.
#                 The script attempts to create this directory if it does not exist.A
# UID: The user ID for the new user.  Leave blank for the script to automatically
#      choose the first unused ID at or above MINUID (currently 501).
 
ADMIN="user"
if [ "$1" = "-a" ] ; then
    ADMIN="admin user"
    shift
fi
 
USERNAME="$1"
LONGNAME="$2"
PRIMARY_GID="$3"
HOMEDIR="$4" # Optional
NEWUID="$5" # Optional
 
MINUID=501
DOMAIN="."
 
# Must have newline here.
IFS="
"
 
# /*!
#     @abstract Checks to see if a long name is reasonable.
#     @discussion Ideally, this should do more checks.
#  */
valid_username()
{
    local NAME="$1"
 
    if [ "$NAME" = "" ] ; then
        return 1;
    fi
 
    return 0;
}
 
# /*!
#     @abstract Checks to see if a long name is reasonable.
#     @discussion
#         Checking for non-empty strings is good enough for now,
#         but ideally, this should also check for duplicates.
#         The code doesn't do this because there's no good way
#         that doesn't involve a huge file and grep.
#  */
valid_longname()
{
    local NAME="$1"
 
    if [ "$NAME" = "" ] ; then
        return 1;
    fi
    return 0
}
 
# /*!
#     @abstract Checks to see if a (numeric) group ID is reasonable.
#  */
valid_gid()
{
    local NEWGID="$1"
 
    # Empty primary GID is illegal.
    if [ "$NEWGID" = "" ] ; then
        return 1;
    fi
 
    local NEWGIDSTR="$(printf "%d" "$NEWGID" 2> /dev/null)"
 
    if [ "$NEWGIDSTR" != "$NEWGID" ] ; then
        return 1;
    fi
 
    return 0;
}
 
# /*!
#     @abstract Checks to see if a (numeric) user ID is reasonable.
#  */
valid_uid()
{
    local NEWUID="$1"
 
    # Empty UID means "choose one for me"
    if [ "$NEWUID" = "" ] ; then
        return 0;
    fi
 
    local NEWUIDSTR="$(printf "%d" "$NEWUID" 2> /dev/null)"
 
    if [ "$NEWUIDSTR" != "$NEWUID" ] ; then
        return 1;
    fi
 
    return 0;
}
 
# /*!
#     @abstract Creates an associative pseudo-array for UID to username mapping.
#  */
initUIDMap()
{
    local SKIPUSER="$1"
 
    local USERS="$(dscl "$DOMAIN" -list /Users)"
 
    for i in $USERS ; do
        if [ "$i" != "$SKIPUSER" ] ; then
            eval "UID_$(dscl "$DOMAIN" -read /Users/"$i" UniqueID 2>/dev/null | sed 's/UniqueID: //' | sed 's/-/MINUS/')=\"$i\""
        fi
    done
}
 
# /*!
#     @abstract Looks up a UID in the pseudo-array and maps it to a username
#  */
uidToName()
{
    local CHECKUID="$1"
 
    local CHECKUID_ENCODED="$(echo "$CHECKUID" | sed 's/-/MINUS/')"
 
 
    eval echo '$UID_'$CHECKUID_ENCODED
}
 
# /*!
#     @abstract Finds the next unused UID.
#  */
assignUID()
{
    initUIDMap
 
    # An error here means somebody screwed up MINUID.
    local POS=$MINUID
 
    while true ; do
        # echo "Trying $POS" 1>&2
        local TEMPNAME="$(uidToName $POS)"
        if [ "$TEMPNAME" = "" ] ; then
            echo $POS
            return;
        fi
        POS="$(expr $POS '+' 1)"
    done
}
 
# /*!
#     @abstract Returns success if no other user has the chosen UID.
#  */
uid_not_conflicting()
{
    local NEWUID="$1"
    local NEWUSER="$2"
 
    initUIDMap "$NEWUSER"
 
    local TEMPNAME="$(uidToName "$NEWUID")"
 
    if [ "$TEMPNAME" != "" ] ; then
        return 1;
    fi
 
    return 0
}
 
 
while ! valid_username "$USERNAME" ; do
    printf "Enter username: "
    read USERNAME
done
 
while ! valid_uid  "$NEWUID" ; do
    printf "Invalid UID specified.  Enter desired UID: "
    read NEWUID
done
 
while ! valid_gid  "$PRIMARY_GID" ; do
    printf "Invalid group ID specified.  Enter desired GID: "
    read PRIMARY_GID
done
 
while ! valid_longname "$LONGNAME" ; do
    printf "Invalid long name specified.  Enter desired long name: "
    read LONGNAME
done
 
 
# Test code
### echo "UID Conflict check:"
### uid_not_conflicting "501" "dg" # Test this first or else.
### echo "$? should be 0"
### uid_not_conflicting "501" "Schlomo"
### echo "$? should be 1"
 
### echo "First free UID is $(assignUID)"
 
dscl $DOMAIN -read /Users/"$USERNAME" > /dev/null 2>&1
if [ $? = 0 ] ; then
    echo "Failed.  A user with that name already exists.." 1>&2
    exit -1
fi
 
dscl $DOMAIN -create /Users/"$USERNAME"
 
if [ $? != 0 ] ; then
    echo "Failed.  User could not be created." 1>&2
    exit -1
fi
 
dscl $DOMAIN -create /Users/"$USERNAME" UserShell /bin/bash
dscl $DOMAIN -create /Users/"$USERNAME" RealName "$LONGNAME"
if [ "$NEWUID" = "" ] ; then
    NEWUID="$(assignUID)"
fi
dscl $DOMAIN -create /Users/"$USERNAME" UniqueID $NEWUID
 
while ! uid_not_conflicting "$NEWUID" "$USERNAME"; do
    echo "A user with ID $NEWUID exists already.  Assigning a new UID." 1>&2
    OLDUID="$NEWUID"
    NEWUID="$(assignUID)"
    dscl $DOMAIN -change /Users/"$USERNAME" UniqueID "$OLDUID" "$NEWUID"
done
 
dscl $DOMAIN -create /Users/"$USERNAME" PrimaryGroupID $PRIMARY_GID
 
if [ "$HOMEDIR" = "" ] ; then
    dscl $DOMAIN -create /Users/"$USERNAME" NFSHomeDirectory /Users/"$USERNAME"
    if [ ! -d "/Users/$USERNAME" ] ; then
        mkdir "/Users/$USERNAME"
    fi
else
    dscl $DOMAIN -create /Users/"$USERNAME" NFSHomeDirectory "$HOMEDIR";
fi
 
dscl $DOMAIN -passwd /Users/"$USERNAME" "*"
# passwd "$USERNAME"
 
UUID="$(/usr/bin/uuidgen)"
dscl $DOMAIN -create /Users/"$USERNAME" GeneratedUID "$UUID"
 
if [ "$ADMIN" = "admin user" ] ; then
    dscl $DOMAIN -append /Groups/admin GroupMembership "$USERNAME"
    dscl $DOMAIN -append /Groups/admin GroupMembers "$UUID"
fi
 
echo "Added $ADMIN $USERNAME with ID $NEWUID and UID $UUID.  Please remember to set a password for the user."
 

  Сценарий перечисления D-11 для добавления новой группы, использующей dscl (addgroup.sh)

#!/bin/sh
 
# Usage:
#
# addgroup <GROUPNAME> <LONGNAME> [<GID> ]
#
# GROUPNAME: The OS X "short name", e.g. admin
# LONGNAME: The OS X "real name", e.g. "Administrators"
# GID: The group ID for the new group.  Leave blank for the script to automatically
#      choose the first unused ID at or above MINGID (currently 501).
#
 
 
GROUPNAME="$1"
LONGNAME="$2"
NEWGID="$3" # Optional
 
MINGID=501
 
DOMAIN="."
 
# Must have newline here.
IFS="
"
 
ADDGROUP="./addgroup.sh"
 
if [ -f "/usr/local/bin/addgroup" ] ; then
    ADDGROUP="/usr/local/bin/addgroup"
fi
 
# /*!
#     @abstract Checks to see if a group long name is reasonable.
#     @discussion
#         Checking for non-empty strings is good enough for now,
#         but ideally, this should also check for duplicates.
#         The code doesn't do this because there's no good way
#         that doesn't involve a huge file and grep.
#  */
valid_longname()
{
    local NAME="$1"
 
    if [ "$NAME" = "" ] ; then
        return 1;
    fi
 
    return 0;
}
 
# /*!
#     @abstract Checks to see if a group name is reasonable.
#     @discussion Ideally, this should do more checks.
#  */
valid_groupname()
{
    local NAME="$1"
 
    if [ "$NAME" = "" ] ; then
        return 1;
    fi
    return 0
}
 
 
# /*!
#     @abstract Checks to see if a (numeric) group ID is reasonable.
#  */
valid_gid()
{
    local NEWGID="$1"
 
    # Empty primary GID means "choose one for me"
    if [ "$NEWGID" = "" ] ; then
        return 0;
    fi
 
    local NEWGIDSTR="$(printf "%d" "$NEWGID" 2> /dev/null)"
 
    if [ "$NEWGIDSTR" != "$NEWGID" ] ; then
        return 1;
    fi
 
    return 0;
}
 
 
# /*!
#     @abstract Creates an associative pseudo-array for GID to username mapping.
#  */
initGIDMap()
{
    local SKIPGROUP="$1"
 
    # GROUPS is BASH reserved word
    local ALLGROUPS="$(dscl "$DOMAIN" -list /Groups)"
 
    for i in $ALLGROUPS ; do
        if [ "$i" != "$SKIPGROUP" ] ; then
            eval "GID_$(dscl "$DOMAIN" -read /Groups/"$i" PrimaryGroupID 2>/dev/null | sed 's/PrimaryGroupID: //' | sed 's/-/MINUS/')=\"$i\""
        fi
    done
}
 
 
# /*!
#     @abstract Looks up a GID in the pseudo-array and maps it to a group name
#  */
gidToName()
{
    local CHECKGID="$1"
 
    local CHECKGID_ENCODED="$(echo "$CHECKGID" | sed 's/-/MINUS/')"
 
 
    eval echo '$GID_'$CHECKGID_ENCODED
}
 
# /*!
#     @abstract Finds the next unused UID.
#  */
assignGID()
{
    initGIDMap
 
    # An error here means somebody screwed up MINGID.
    local POS=$MINGID
 
    while true ; do
        # echo "Trying $POS" 1>&2
        local TEMPNAME="$(gidToName $POS)"
        if [ "$TEMPNAME" = "" ] ; then
            echo $POS
            return;
        fi
        POS="$(expr $POS '+' 1)"
    done
}
 
# /*!
#     @abstract Returns success if no other group has the chosen GID.
#  */
gid_not_conflicting()
{
    local NEWGID="$1"
    local NEWGROUP="$2"
 
    initGIDMap "$NEWGROUP"
 
    local TEMPNAME="$(gidToName "$NEWGID")"
 
    if [ "$TEMPNAME" != "" ] ; then
        return 1;
    fi
 
    return 0
}
 
 
while ! valid_groupname "$GROUPNAME" ; do
    printf "Enter group name: "
    read GROUPNAME
done
 
while ! valid_gid  "$NEWGID" ; do
    printf "Invalid or no group ID specified.  Enter desired GID: "
    read NEWGID
done
 
while ! valid_longname "$LONGNAME" ; do
    printf "Invalid long name specified.  Enter desired long name: "
    read LONGNAME
done
 
 
# Test code
# echo "GID Conflict check:"
# gid_not_conflicting "80" "admin" # Test this first or else.
# echo "$? should be 0"
# gid_not_conflicting "80" "Schlomo"
# echo "$? should be 1"
 
echo "First free GID is $(assignGID)"
 
dscl $DOMAIN -read /Groups/"$GROUPNAME" > /dev/null 2>&1
if [ $? = 0 ] ; then
    echo "Failed.  A group with that name already exists.." 1>&2
    exit -1
fi
 
dscl $DOMAIN -create /Groups/"$GROUPNAME"
 
if [ $? != 0 ] ; then
    echo "Failed.  Group could not be created." 1>&2
    exit -1
fi
 
dscl $DOMAIN -create /Groups/"$GROUPNAME" RealName "$LONGNAME"
if [ "$NEWGID" = "" ] ; then
    NEWGID="$(assignGID)"
fi
dscl $DOMAIN -create /Groups/"$GROUPNAME" PrimaryGroupID $NEWGID
 
while ! gid_not_conflicting "$NEWGID" "$GROUPNAME"; do
    echo "A user with ID $NEWGID exists already.  Assigning a new GID." 1>&2
    OLDGID="$NEWGID"
    NEWGID="$(assignGID)"
    dscl $DOMAIN -change /Groups/"$GROUPNAME" PrimaryGroupID "$OLDGID" "$NEWGID"
done
 
UUID="$(/usr/bin/uuidgen)"
dscl $DOMAIN -create /Groups/"$GROUPNAME" GeneratedUID "$UUID";
 
# Legacy UNIX group password
dscl $DOMAIN -create /Groups/"$GROUPNAME" Password "*"
 
echo "Added $GROUPNAME with ID $NEWGID and UUID $UUID.  Please remember to set a password for the user."