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

Когда уместен интерфейс командной строки?

Интерфейсы командной строки идеально подходят для выполнения простых операций на основе шаблонов, которые напрямую управляют операционной системой. Есть несколько хорошо известных интерфейсов командной строки, которые экономят тысячи часов для разработчиков по всему миру. Одним из наиболее широко используемых интерфейсов командной строки является git. По своей сути Git - это инструмент, который помогает разработчикам управлять версиями данной кодовой базы с помощью набора команд. С помощью нескольких простых команд в терминале интерфейс командной строки git может инициализировать репозитории, отслеживать изменения, объединять изменения и даже отправлять эти изменения в удаленный репозиторий (GitHub, GitLab, Bitbucket и т. Д.). Git - отличный пример того, как максимально эффективно использовать интерфейс командной строки. Несмотря на то, что он выполняет большую часть тяжелой работы в фоновом режиме, общий процесс и выполнение внутри машины являются алгоритмическими и основаны на базовом распознавании образов.

Чему мы научимся и чего добьемся

В этом руководстве мы изучим основы сценария оболочки и воспользуемся упрощенным языком для создания интерфейса командной строки, который создает простую рабочую область Ruby и добавляет базовые элементы в базу кода.

Если вы в какой-то момент застрянете, загляните в репозиторий, связанный с этим руководством. Каждая ветка охватывает отдельный раздел учебника.

Изучите функциональные возможности интерфейса командной строки

Перво-наперво, мы должны придумать имя CLI. Поскольку все интерфейсы командной строки вводятся в терминалы, предпочтительнее использовать короткое, но описательное имя. Вы же не хотите, чтобы ваши пользователи печатали предложение только для выполнения одной команды. Поскольку наш интерфейс командной строки специально предназначен для создания рабочих пространств Ruby и управления ими, назовем его buildr. Теперь давайте разберемся, как выглядит базовое рабочее пространство Ruby. Простое рабочее пространство Ruby обычно содержит каталог config, lib и test, а также файлы Gemfile, Rakefile, .gitignore и README.md. Ниже показано, что мы ожидаем как пользователь, когда запускаем наш интерфейс командной строки для создания рабочего пространства Ruby.

./<app name>/
├── config/
│   └── .keep
├── lib/
│   └── .keep
├── test/
│   └── test_helper.rb
├── .gitignore
├── Gemfile
├── Rakefile
└── README.md

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

  • buildr new <app name> - Создает простую файловую структуру для проекта Ruby с использованием <app name>.
  • buildr generate <component> <name> - строит заданный <component> с использованием заданного <name> и сохраняет его в правильном каталоге.

Понимание сценария оболочки

Сценарий оболочки - это программа, запускаемая внутри оболочки Unix. Существует несколько языков сценариев, которые по сути являются разными диалектами сценариев оболочки (sh, bash, ssh и т. Д.). Самым распространенным языком сценариев, широко используемым сегодня, является Bourne Shell (sh). В этом уроке мы будем использовать Bourne Again Shell (bash). Между sh и bash есть различия, но эти различия выходят за рамки данного руководства. Если вы хотите узнать больше, ознакомьтесь с этим ответом на StackOverflow.

Bash очень похож на многие языки нижнего уровня и включает в себя основы, такие как циклы, логические значения, строки, целые числа и т. Д. Истинная сила Bash (или любого языка сценариев) заключается в его способности эффективно перемещаться и управлять операциями на основе Unix. система напрямую. В этом руководстве мы не будем слишком углубляться в глубины bash, и я уверен, что любой сможет следовать вместе с ним, лишь немного разбираясь в bash. Если вы хотите узнать больше о языке сценариев sh, ознакомьтесь с этим руководством. Он содержит множество основ sh и является отличной доской для тех, кто плохо знаком с языками сценариев.

Настройка рабочего пространства Bash

Прежде чем мы напишем хоть одну строчку кода, нам нужно убедиться, что мы настроили себя на успех, создав прочный фундамент. В этом руководстве мы будем практиковать разработку через тестирование или TDD. При этом нам нужно сначала установить летучие мыши (Bash Automated Testing System). Перейдите в репозиторий GitHub, выполните клонирование, перейдите в репозиторий и запустите ./install.sh /usr/local. Теперь, когда bats установлен, давайте настроим файловую структуру кодовой базы нашего проекта.

Создайте каталог с именем buildr и перейдите в этот каталог (mkdir buildr && cd buildr). Внутри каталога проекта мы сделаем три каталога (mkdir {config,lib,test}). В корне проекта мы также создадим три пустых файла (touch {bats,buildr,README.md}). Ваша файловая структура должна выглядеть примерно так.

./buildr/
├── config/
├── lib/
├── test/
├── bats
├── buildr
└── README.md

Давайте рассмотрим, как каждый из этих компонентов в конечном итоге будет служить нашему интерфейсу командной строки.

  • config/ - в каталоге config будут размещены все сценарии, предназначенные для запуска конечным пользователем. Например, мы поместим наш установочный скрипт в папку config. Config также является местом хранения любых файлов метаданных или внутренних документов.
  • lib/ - Каталог lib будет содержать все сценарии, используемые CLI для выполнения команд. Важно отметить, что не существует жестких соглашений для структурирования приложений bash; тем не менее, я склонен следовать некоторой форме соглашения о микросервисах, когда каждая команда получает свой собственный каталог и набор сценариев в каталоге lib. Мы рассмотрим это подробнее по мере продолжения создания интерфейса командной строки.
  • test/ - Каталог test будет содержать все наши тестовые файлы вместе с test_helper и любой необходимой информацией для создания заглушек.
  • bats - летучая мышь будет сценарием, написанным для запуска всех тестов одной командой. Обратите внимание, что bats в нашем проекте отличается от установленной нами системы тестирования летучих мышей. Скрипт bats будет использовать систему тестирования и предназначен исключительно для нашего удобства.
  • buildr - buildr будет исполнителем или исполнителем нашего интерфейса командной строки. Подобно раннерам на других языках, сценарий будет относительно легким и будет иметь достаточно кода, чтобы запустить команду.
  • README.md - README.md будет содержать базовую документацию по нашему интерфейсу командной строки, включая информацию об установке и функциональности.

Основа нашей рабочей области почти завершена, нам просто нужно изменить права доступа на bats и buildr, чтобы все пользователи могли запускать файлы и использовать сценарии, которые мы разместим в них. Для этого нам потребуется использовать команду chmod. Введите chmod 755 {bats,buildr}. Давайте также создадим сценарий bats, чтобы все, что нам нужно было сделать для запуска всех тестов, - это ввести ./bats из корня проекта. Откройте файл bats и вставьте следующий код.

# ./bats
#!/bin/sh
echo "Testing all test files ./test/*_test.sh"
bats ./test/*_test.sh

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

Проверка нашей первой команды

Наш фундамент заложен, рабочее пространство создано, теперь пора начать самое интересное. Давайте напишем наш первый тест и начнем строить наш интерфейс командной строки.

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

  • buildr new <app name> - строит файловую структуру Ruby.
  • buildr generate <component> <name> - строит заданный <component>, используя заданный <name>, и сохраняет его в правильном каталоге.

Перво-наперво, давайте проверим, можем ли мы выполнить команду buildr new <app name>. Внутри каталога test/ создайте test_helper.bash и buildr_test.sh. Нам понадобится несколько необходимых переменных в нашем файле test_helper. А пока давайте просто установим для переменной BUILDR путь к сценарию ./buildr и установим переменную, обозначающую RELATIVE_PATH тестовых файлов ./test/.

# ./test/test_helper.bash
#!/bin/sh
RELATIVE_PATH="./test/"
BUILDR="../buildr"

Обратите внимание, что между именем переменной и сохраняемой строкой нет пробелов. Сценарий оболочки считает пробел началом другого аргумента, то есть BUILDR = "./buildr" интерпретируется как запуск BUILDR, затем запуск =, затем запуск "./buildr", что, конечно же, приведет к ошибке.

Теперь, когда наш test_helper установлен, давайте напишем первый тест. Для начала мы хотим убедиться, что запуск buildr new без имени проекта выводит на терминал ошибку, в которой указывается, что имя необходимо указать, и завершает работу с кодом 1. Имея это в виду, давайте откроем ./test/buildr_test.sh и вставим следующий код.

# ./test/buildr_test.sh
#!/usr/bin/env bats
load test_helper
setup() {
  rm -rf testing && mkdir testing
  cd testing
}
teardown() {
  cd ..
  rm -rf testing
}
@test "it executes new command" {
  run $BUILDR new
  [ "$status" -eq 1 ]
  [ "${lines[0]}" = "Error: A project name must be given after the command 'new'" ]
}

Давайте посмотрим, что здесь происходит. Начнем с shebang, определяющего, что код должен запускаться с использованием летучих мышей. После этого у нас есть строка load test_helper. Это команда bats, которая загружает содержимое в test_helper.sh. Затем мы определяем задачи, которые должны быть выполнены на setup() и teardown(). Это также функции летучих мышей, которые выполняются до и после каждого теста соответственно. Наконец, мы определяем наш первый тест, используя формат летучих мышей @test "..." {}. Внутри теста мы запускаем $BUILDR new, который определен как ./buildr в test_helper. Мы ожидаем, что статус будет 1, а первая строка вывода будет «Ошибка: имя проекта должно быть указано после команды« новый ».

Давайте запустим код, введя ./bats в корне проекта. Вы должны увидеть следующий ответ.

Testing all test files ./test/*_test.sh
 ✗ it executes new command
   (in test file test/buildr_test.sh, line 18)
     `[ "$status" -eq 1 ]' failed
1 test, 1 failure

Очевидно, тест не проходит, потому что мы еще ничего не закодировали ... Давайте напишем код, чтобы этот тест прошел.

Мы выполняем команду buildr new, поэтому нам нужно открыть ./buildr и указать функциональные возможности для new. Помните, что в соответствии с принципами TDD необходимо написать столько кода, сколько нужно для прохождения теста. С учетом сказанного нам нужно написать достаточно кода для обработки команды new без передачи имени проекта.

# ./buildr
#!/bin/bash
case $1 in
  new)
    echo "Error: A project name must be given after the command 'new'"
    exit 1
    ;;
  *)
    echo "Error: Unrecognized command. Please refer to the documentation for all functionality."
    exit 1
    ;;
esac

Давайте посмотрим, что происходит в нашем ./buildr файле. Во-первых, мы определяем, что файл написан с использованием языка сценариев bash с помощью shebang #!/bin/bash. После этого у нас есть оператор case, обращающийся к переменной $1. В оболочке все, что находится после исходного файла, сохраняется как переменная в порядке возрастания от $1 до $9. Этот оператор case смотрит конкретно на первый аргумент, которым в нашем тесте является new. Если он новый, как определено в new), мы выводим эхо «Ошибка: имя проекта должно быть указано после команды« новый »и выходим с кодом состояния, равным единице. Если передана какая-либо команда, не определенная в операторе case, она попадет в категорию *), и на экран будет выведена ошибка.

Теперь выполнение тестов с ./bats должно привести к успеху, и, пройдя этот тест, мы официально создали интерфейс командной строки. Однако в своем текущем состоянии этот интерфейс командной строки мало что делает, кроме вывода одного сообщения, если было передано новое, и другого для любой другой ситуации. Давайте напишем второй тест в buildr_test.sh для дальнейшего построения нашей new функции.

# ./test/buildr_test.sh
@test "it executes new command with project name" {
  run $BUILDR new testrb
  [ "$status" -eq 0 ]
  [ "${lines[0]}" = "Building workspace for project 'testrb'" ]
}

Хорошо, наш второй тест находится в нашем наборе тестов. Давайте запустим ./bats, чтобы увидеть, какие ошибки мы получаем во втором тесте.

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✗ it executes new command with project name
   (in test file test/buildr_test.sh, line 25)
     `[ "$status" -eq 0 ]' failed
2 tests, 1 failure

Это не удается, потому что сейчас при вызове команды new будет выведена только ошибка и будет выполнен выход с кодом состояния 1, вне зависимости от того, передано ли имя проекта. Давайте вернемся к ./buildr и добавим функции, прошедшие наш последний тест.

# ./buildr
#!/bin/bash
case $1 in
  new)
    shift
    if [ "$1" = "" ]; then
      echo "Error: A project name must be given after the command 'new'"
      exit 1
    else
      echo "Building workspace for project '${1}'"
    fi
    ;;
  *)
    echo "Error: Unrecognized command. Please refer to the documentation for all functionality."
    exit 1
    ;;
esac

Обратите внимание: если команда new, мы немедленно вызываем shift. Это сдвинет все аргументы на одну переменную вниз. Например, если мы вызвали buildr new this and that, изначально переменные назначаются как $1=new; $2=this; $3=and; $4=that. После вызова shift переменные выглядят следующим образом: $1=this; $2=and; $3=that. Итак, в нашем ./buildr мы вызываем shift, а затем проверяем, является ли $1 пустым. Если он пуст, мы отображаем нашу ошибку и выходим с кодом состояния, равным единице, в противном случае мы начинаем выполнение команды new.

Давайте проведем наши тесты…

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✓ it executes new command with project name
2 tests, 0 failures

Здорово! Мы проходим тесты, но в нашем ./buildr файле становится слишком много логики. Помните, что файл runner должен только маршрутизировать команды и выполнять необходимый код. Давайте реорганизуем часть кода из раннера в его собственную функцию.

Используя принципы, лежащие в основе микросервисов, мы создадим новый каталог, названный в честь создаваемой нами функции, ./lib/new/. Внутри ./lib/new/ мы создадим новый сценарий new.sh. Здесь мы разместим всю логику и функции команды new.

# ./lib/new/new.sh
#!/bin/bash
execute_new_command() {
  if [ "$1" = "" ]; then
    echo "Error: A project name must be given after the command 'new'"
    exit 1
  else
    echo "Building workspace for project '${1}'"
  fi
}

Как видите, мы просто взяли тот же оператор if из ./buildr и поместили его в функцию execute_new_command(), которая находится в ./lib/new/new.sh. А теперь подключим ./buildr к этому файлу.

# ./buildr
#!/bin/bash
export PARENT_PATH=$(cd "$(dirname "$0")"; pwd)
source $PARENT_PATH/lib/new/new.sh
case $1 in
  new)
    shift
    execute_new_command $@
    ;;
  *)
    echo "Error: Unrecognized command. Please refer to the documentation for all functionality."
    exit 1
    ;;
esac

Какие новости? Итак, мы создали переменную PARENT_PATH и экспортируем ее как глобальную для всех файлов. Мы также сейчас работаем source на ./lib/new/new.sh. Функция source по существу выполняет весь код в данном файле и позволяет текущему сценарию запускать любой из этого кода, как если бы он был его собственным. Следовательно, внутри new) мы можем вызвать функцию execute_new_command и передать ей все аргументы ($@). Давайте проведем наши тесты и убедимся, что мы ничего не сломали.

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✓ it executes new command with project name
2 tests, 0 failures

Теперь, когда мы освоились с TDD и процессом написания bash. Приступим к созданию функциональности FileIO команды new.

Создание первой команды

Прежде чем писать наш первый тест для команды new, давайте подумаем о том, что мы на самом деле тестируем. После запуска buildr new testing мы ожидаем получить каталог с именем переданного имени проекта (testing/) с config/, lib/ и test/ вместе с несколькими другими файлами Ruby. Давайте пока проигнорируем это и напишем модульный тест, подтверждающий, что мы создаем исходные каталоги.

Мы создадим новый тестовый файл в каталоге test, названный в честь функции, которую мы тестируем, в нашем случае /test/new_test.sh. Внутри new_test давайте напишем наш первый модульный тест, при тестировании он создает каталог с заданным именем вместе с config, lib и test.

# ./test/new_test.sh
#!/usr/bin/env bats
load test_helper
setup() {
  source ./lib/new/new.sh
  rm -rf testing && mkdir testing
  cd testing
}
teardown() {
  cd ..
  rm -rf testing
}
@test "it creates the base directories of a Ruby workspace" {
  create_base_directories testrb
  [ -d "./testrb/" ]
  [ -d "./testrb/lib/" ]
  [ -d "./testrb/config/" ]
  [ -d "./testrb/test/" ]
}

Обратите внимание, что в этом тесте мы не выполняем никаких команд с run. Вместо этого мы получаем new.sh и вызываем функции непосредственно внутри теста (в данном случае create_base_folders). Это один из способов запуска модульных тестов в bash, гарантирующий, что мы не задействуем никакие другие функции, кроме тех, которые мы хотим протестировать. В нашем тесте мы также передаем флаг -d. Это просто проверяет, является ли следующая строка каталогом. Хорошо, давайте проведем тест.

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✓ it executes new command with project name
 ✗ it creates the base directories of a Ruby workspace
   (in test file test/new_test.sh, line 19)
     `[ -d "./testrb/" ]' failed
3 tests, 1 failure

Ни для кого не удивительно, что тест не прошел, заявив, что каталог testrb не был найден. Давайте запрограммируем это и пройдем тест.

# ./lib/new/new.sh
create_base_directories() {
  mkdir $1
  mkdir {$1/config,$1/lib,$1/test}
  touch $1/config/.keep && touch $1/lib/.keep
}

Внутри new.sh (поскольку это файл, который мы получили и тестируем) мы добавили функцию create_base_directories. Эта функция ожидает один параметр, который является именем проекта, и создает этот каталог вместе с соответствующими каталогами config, lib и test. Давайте проведем тест и посмотрим, соответствует ли он нашим ожиданиям.

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✓ it executes new command with project name
 ✓ it creates the base directories of a Ruby workspace
3 tests, 0 failures

Теперь, когда мы генерируем базовые каталоги, давайте напишем тест для каждого из файлов, сгенерированных в корне (.gitignore, Gemfile, Rakefile и README.md). Поскольку это выходит за рамки данного руководства, я не буду по отдельности рассматривать весь код внутри каждого из этих файлов, но мы рассмотрим, как сгенерировать файлы с кодом внутри него.

Напишем тест для генерации .gitignore.

# ./test/new_test.sh
@test "it creates .gitignore file with contents" {
  create_gitignore_file
  [ -e "./.gitignore" ]
  [ "$(head -n 1 .gitignore)" = "# Generated by buildr CLI" ]
}

Вы заметите, что этот тест делает несколько вещей, отличных от предыдущих. Во-первых, вместо передачи -d для проверки наличия каталога мы передаем -e, который проверяет, существует ли файл. Во-вторых, мы читаем первую строку файла, чтобы убедиться, что в нем присутствует контент, который мы хотели сохранить. Этот тест завершится неудачно, поскольку мы не написали create_gitignore_file() функцию в ./lib/new/new.sh. Напишем эту функцию.

# ./lib/new/new.sh
create_gitignore_file() {
  touch .gitignore
  printf "%s\n" "# Generated by buildr CLI" "" \
                "# Bundler generated files" "/.bundle/" \
                "" "/tmp/" > .gitignore
}

Обратите внимание, что в нашей последней функции после создания файла .gitignore мы используем printf "%s\n" для хранения кода, который нам нужен в файле. Вы можете узнать больше о printf на страницах руководства Linux, но наша функция просто принимает текст как строку, создает новую строку в конце каждой строки и сохраняет ее в .gitignore вместо stdout. Есть и другие способы записи текста в файлы, но я обнаружил, что это наиболее простой способ сделать это.

Теперь мы будем следовать этому же шаблону для остальных файлов (Gemfile, Rakefile и README.md).

# ./test/new_test.sh
@test "it creates Gemfile file with contents" {
  create_gemfile_file
  [ -e "./Gemfile" ]
  [ "$(head -n 1 Gemfile)" = "source \"https://rubygems.org\"" ]
}
@test "it creates Rakefile file with contents" {
  create_rakefile_file
  [ -e "./Rakefile" ]
  [ "$(head -n 1 Rakefile)" = "require 'rake'" ]
}
@test "it creates README file with contents" {
  create_readme_file testrb
  [ -e "./README.md" ]
  [ "$(head -n 1 README.md)" = "# testrb README" ]
}

Наши тесты написаны, теперь давайте напишем код, удовлетворяющий всем новым тестам.

# ./lib/new/new.sh
create_gemfile_file() {
  touch Gemfile
  printf "%s\n" "source \"https://rubygems.org\"" "" "gem 'pry'" \
                "gem 'rake'" "" "group :test do" \
                "  gem 'minitest'" "end" > Gemfile
}
create_rakefile_file() {
  touch Rakefile
  printf "%s\n" "require 'rake'" "" "namespace :test do" \
                "  desc 'Run all tests in test suite'" \
                "  task :all do" \
                "    Dir.glob('./test/*_test.rb').each { |file| require file}" "  end" "end" > Rakefile
}
create_readme_file() {
  touch README.md
  printf "%s\n" "# $1 README" "" "Generated by buildr." \
                "Under construction." > README.md
}

Хорошо. Все наши функции написаны для создания каталогов и корневых файлов. Давайте запустим наши тесты и убедимся, что все функции работают так, как мы от них ожидаем.

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✓ it executes new command with project name
 ✓ it creates the base directories of a Ruby workspace
 ✓ it creates .gitignore file with contents
 ✓ it creates Gemfile file with contents
 ✓ it creates Rakefile file with contents
 ✓ it creates README file with contents
7 tests, 0 failures

Все наши функции генерации файлов почти построены, нам просто нужно сгенерировать test_helper.rb, который будет жить в каталоге test. Напишем тест.

# ./test/new_test.sh
@test "it creates test_helper file with contents" {
  create_test_helper_file
  [ -e "./test_helper.rb" ]
  [ "$(head -n 1 test_helper.rb)" = "require 'minitest/autorun'" ]
}

Чтобы пройти тест…

# ./lib/new/new.sh
create_test_helper_file() {
  touch test_helper.rb
  printf "%s\n" "require 'minitest/autorun'" \
                "require 'minitest/pride'" "require 'pry'" \
                "" "PATH = File.expand_path('.')" \
                "ENV['test'] = 'true'" > test_helper.rb
}

И с этим у нас есть все функции, позволяющие нашему CLI генерировать простую рабочую область Ruby. Все, что нам нужно сделать сейчас, это соединить их вместе и подтвердить функциональность с помощью некоторых интеграционных тестов!

Соединяем все вместе

У нас есть все наши отдельные функции, которые работают правильно и генерируют необходимые файлы. Пришло время связать их вместе, чтобы мы могли выполнять все функции с помощью простой команды buildr new <name>.

Прежде всего напишем тест. Вернемся к файлу ./test/buildr_test.sh и напишем интеграционный тест.

# ./test/buildr_test.sh
@test "it generates a complete workspace when new and a name is passed" {
  run $BUILDR new testrb
  [ "$status" -eq 0 ]
  [ -d "./testrb/config" ]
  [ -d "./testrb/lib" ]
  [ -d "./testrb/test" ]
  [ -e "./testrb/.gitignore" ]
  [ -e "./testrb/Gemfile" ]
  [ -e "./testrb/Rakefile" ]
  [ -e "./testrb/README.md" ]
  [ -e "./testrb/test/test_helper.rb" ]
}

В этом тесте мы проверяем, созданы ли все файлы и каталоги с помощью команды new. Поскольку у нас уже есть все эти компоненты, нам просто нужно просто собрать их вместе в файле ./lib/new/new.sh и вызвать их в правильном порядке.

# ./lib/new/new.sh
execute_new_command() {
  if [ "$1" = "" ]; then
    echo "Error: A project name must be given after the command 'new'"
    exit 1
  else
    echo "Building workspace for project '${1}'"
    create_base_directories $1
    cd $1
    create_gitignore_file
    create_gemfile_file
    create_rakefile_file
    create_readme_file $1
    cd test
    create_test_helper_file
    exit
  fi
}

Мы видим, что все, что мы делаем, это добавляем функции, которые мы тестировали в наших модульных тестах, к функции, которую вызывает команда buildr new. Теперь это объединяет все основные функции вместе, чтобы создать законченное рабочее пространство. Давайте проведем все наши тесты.

Testing all test files ./test/*_test.sh
 ✓ it executes new command
 ✓ it executes new command with project name
 ✓ it generates a complete workspace when new and a name is passed
 ✓ it creates the base directories of a Ruby workspace
 ✓ it creates .gitignore file with contents
 ✓ it creates Gemfile file with contents
 ✓ it creates Rakefile file with contents
 ✓ it creates README file with contents
 ✓ it creates test_helper file with contents
9 tests, 0 failures

Здорово! Все девять тестов пройдены, и команда new нашего интерфейса командной строки работает именно так, как нам хотелось бы. Из корня проекта запустите ./buildr new testing и убедитесь сами.

Установка CLI

В нашем интерфейсе командной строки первая команда полностью построена и функционирует; однако запустить его можно только в корневой папке проекта. Это довольно неудобно. Давайте напишем сценарий, который установит его, чтобы его можно было использовать где угодно на нашей машине.

Давайте создадим новый файл touch ./config/install.sh. Чтобы установить CLI, мы должны указать нашей ОС, где найти исходные файлы. Мы можем сделать это, добавив его путь к переменной окружения $PATH. Затем нам нужно сохранить добавленный путь, поместив этот $PATH в наш профиль. В этом уроке я помещу $PATH в наш .bash_profile. Наконец, нам нужно убедиться, что устанавливаемые нами файлы не находятся в каталоге, который можно легко удалить с компьютера. Мне нравится хранить их в пользовательском каталоге bin. Обратите внимание, что это отличается от корневого каталога bin. Теперь, когда мы понимаем, что нужно сделать нашему сценарию установки для успешной установки нашего интерфейса командной строки, давайте напишем код.

# ./config/install.sh
#!/bin/bash
script_pwd=$(pwd)
if [ -d $HOME/bin/buildr ]; then
  echo "buildr is already installed on this machine"
else
  mkdir -p $HOME/bin/buildr && cd $HOME/bin/buildr
  echo "Installing buildr"
  mkdir lib
  cp -r $script_pwd/lib/* ./lib
  cp $script_pwd/buildr .
  echo "Adding buildr to bash commands"
  current_profile=""
  if [ ! -e "${HOME}/.bash_profile" ]; then
    touch $HOME/.bash_profile
    current_profile=$(cat $HOME/.profile)
  else
    current_profile=$(sed '/export PATH/d' $HOME/.bash_profile)
  fi
  printf '%s\n' "export PATH=${HOME}/bin/buildr:${PATH}" \
                "$current_profile" > $HOME/.bash_profile
  echo "Install complete"
fi

Давайте рассмотрим, что делает этот сценарий установки. Прежде всего, мы проверяем, существует ли уже в пользовательском каталоге bin каталог с именем buildr. Если есть, мы предполагаем, что он уже установлен, и выдаем ошибку. В противном случае мы создаем каталог и переходим в него. Оказавшись в каталоге buildr, мы просто копируем файл runner buildr и его зависимости. После этого мы проверяем bash_profile и создаем его, если он не существует. Если это так, мы берем все содержимое этого файла и сохраняем его в переменной current_profile, за исключением строки export PATH. Это просто для уменьшения дублирования и раздутого $PATH. Наконец, мы добавляем строку export PATH, содержащую путь к $HOME/bin/buildr вместе с исходным профилем.

Давайте сделаем этот скрипт исполняемым, запустив chmod 755 ./config/install.sh. После запуска сценария все соединяется вместе, просто перезапустите терминал, и вы сможете запускать buildr new <name> где угодно на машине.

Что дальше?

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

Bash - действительно интересный язык сценариев, который довольно легко освоить. Узнайте, сколько сценариев вы можете создать и какие еще процессы можно автоматизировать. Небо это предел.

Спасибо за чтение и, пожалуйста, оставьте комментарий, если у вас есть какие-либо проблемы или вы знаете, как улучшить это руководство.