Let's Go: программы командной строки с Golang
Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)
Обзор
Язык Go - это захватывающий новый язык, который набирает большую популярность по уважительной причине. В этом уроке вы узнаете, как писать программы для командной строки с помощью Go. Пример программы называется multi-git, и она позволяет вам выполнять команды git одновременно в нескольких репозиториях.
Краткое введение в Go
Go - это C-подобный язык с открытым исходным кодом, созданный в Google некоторыми из первоначальных хакеров C и Unix, которые были мотивированы своей неприязнью к C ++. Это отображено в дизайне Go, который сделал несколько неортодоксальных выборов, таких как отказ от наследования реализации, шаблонов и исключений. Go прост, надежен и эффективен. Его наиболее отличительной особенностью является явная поддержка параллельного программирования через так называемые goroutines и каналы.
Прежде чем приступить к анализу примера программы, следуйте официальному руководству, чтобы подготовиться к разработке на Go.
Программа Multi-Git
Программа multi-git - это простая, но полезная программа Go. Если вы работаете в команде, в которой кодовая база разбита по нескольким git-репозиториям, вам часто приходится вносить изменения в несколько репозиториев. Это проблема, потому что у git нет концепции нескольких репозиториев. Все вращается вокруг одного хранилища.
Это становится особенно проблематичным, если вы используете ветки. Если вы работаете с функцией, которая касается трех репозиториев, вам нужно будет создать ветку в каждом из этих репозиториев, а затем не забывать проверять, извлекать, толкать и объединять их все одновременно. Это не тривиально. Multi-git управляет набором репозиториев и позволяет вам работать со всем набором одновременно. Обратите внимание, что текущая версия multi-git требует, чтобы вы создавали ветви по отдельности, но я могу добавить эту функцию позже.
Изучив способ реализации multi-git, вы многое узнаете о написании программ для командной строки на Go.
Пакеты и импорт
Программы Go организованы в пакеты. Программа multi-git состоит из одного файла с именем main.go. В верхней части файла указано имя пакета 'main', а затем список импорта. Импортируются другие пакеты, используемые multi-git.
1 |
package main |
2 |
|
3 |
|
4 |
|
5 |
import ( |
6 |
|
7 |
"flag"
|
8 |
|
9 |
"fmt"
|
10 |
|
11 |
"log"
|
12 |
|
13 |
"os"
|
14 |
|
15 |
"strings"
|
16 |
|
17 |
"os/exec"
|
18 |
|
19 |
)
|
Например, пакет fmt используется для форматированного ввода-вывода, подобного printf и scanf в C. Go поддерживает установку пакетов из различных источников с помощью команды go get. Когда вы устанавливаете пакеты, они попадают в пространство имен в переменной среды $GOPATH. Вы можете устанавливать пакеты из различных источников, таких как GitHub, Bitbucket, код Google, Launchpad и даже сервисы IBM DevOps, используя несколько распространенных форматов контроля версий, таких как git, subversion, mercurial и bazaar.
Аргументы командной строки
Аргументы командной строки являются одной из наиболее распространенных форм предоставления ввода программам. Они просты в использовании, позволяют запускать и настраивать программу в одну строку и имеют отличную поддержку синтаксического анализа на многих языках. Go называет их «флагами» командной строки и имеет пакет flag для указания и анализа аргументов командной строки (или флагов).
Как правило, вы анализируете аргументы командной строки в начале вашей программы, и multi-git следует этому соглашению. Точкой входа является функция main(). Первые две строки определяют два флага, называемые «command» и «ignoreErrors». Каждый флаг имеет имя, тип данных, значение по умолчанию и строку справки. Вызов flag.Parse() проанализирует фактическую командную строку, переданную программе, и заполнит определенные флаги.
1 |
func main() { |
2 |
|
3 |
command := flag.String("command", "", "The git command") |
4 |
|
5 |
ignoreErrors := flag.Bool( |
6 |
|
7 |
"ignore-errors", |
8 |
|
9 |
false, |
10 |
|
11 |
"Keep running after error if true") |
12 |
|
13 |
flag.Parse() |
Также возможно получить доступ к неопределенным аргументам через функцию flag.Args(). Таким образом, флаги обозначают предопределенные аргументы, а «аргументы» - необработанные аргументы. Необработанные аргументы индексируются на основе 0.
Переменные среды
Другая распространенная форма конфигурации программы - переменные среды. Когда вы используете переменные среды, вы можете запускать одну и ту же программу несколько раз в одной и той же среде, и во всех запусках будут использоваться одни и те же переменные среды.
Multi-git использует две переменные среды: "MG_ROOT" и "MG_REPOS". Multi-git предназначен для управления группой репозиториев git, имеющих общий родительский каталог. Это "MG_ROOT". Имена хранилища указываются в MG_REPOS в виде строки, разделенной запятыми. Чтобы прочитать значение переменной окружения, вы можете использовать функцию os.Getenv().
1 |
// Get managed repos from environment variables
|
2 |
|
3 |
root := os.Getenv("MG_ROOT") |
4 |
|
5 |
if root[len(root) - 1] != '/' { |
6 |
|
7 |
root += "/" |
8 |
|
9 |
}
|
10 |
|
11 |
|
12 |
|
13 |
repo_names := strings.Split(os.Getenv("MG_REPOS"), ",") |
Проверка списка репозитория
Теперь, когда он нашел корневой каталог и имена всех репозиториев, multi-git проверяет, что каждый репозиторий существует под root и что это действительно git-репозиторий. Проверка так же проста, как поиск подкаталога .git для каждого каталога репозитория.
Сначала определяется массив строк с именем «repos». Затем он перебирает все имена репо и создает путь к репозиторию путем объединения корневого каталога и имени репо. Если вызов [os.Stat()]() завершается неудачно для подкаталога .git, он регистрирует ошибку и завершает работу. В противном случае путь к хранилищу добавляется к массиву репозитория.
1 |
var repos []string |
2 |
|
3 |
// Verify all repos exist and are actually git repos (have .git sub-dir)
|
4 |
|
5 |
for _, r := range repo_names { |
6 |
|
7 |
path := root + r |
8 |
|
9 |
_, err := os.Stat(path + "/.git") |
10 |
|
11 |
if err != nil { |
12 |
|
13 |
log.Fatal(err) |
14 |
|
15 |
}
|
16 |
|
17 |
repos = append(repos, path) |
18 |
|
19 |
}
|
Go имеет уникальную возможность обработки ошибок, где функции часто возвращают как возвращаемое значение, так и объект ошибки. Посмотрите, как os.Stat() возвращает два значения. В этом случае заполнитель "_" используется для хранения фактического результата, потому что вы заботитесь только об ошибке. Go очень строг и требует использования именованных переменных. Если вы не планируете использовать значение, вам следует присвоить его «_», чтобы избежать ошибки компиляции.
Выполнение команд оболочки
На данный момент у вас есть список путей к хранилищам, где мы хотим выполнить команду git. Как вы помните, мы получили командную строку git в виде одного аргумента командной строки (флага), называемого «command». Его нужно разделить на массив компонентов (команда git, подкоманда и опции). Вся команда в виде строки также хранится для отображения.
1 |
// Break the git command into components (needed to execute)
|
2 |
|
3 |
var git_components []string |
4 |
|
5 |
for _, component := range strings.Split(*command, " ") { |
6 |
|
7 |
git_components = append(git_components, component) |
8 |
|
9 |
}
|
10 |
|
11 |
command_string := "git " + *command |
Теперь все готово для перебора каждого репозитория и выполнения команды git в каждом из них. Снова используется циклическая конструкция for ... range. Во-первых, multi-git меняет свой рабочий каталог на текущий целевой репозиторий «r» и печатает команду git. Затем он выполняет команду с помощью функции exec.Command() и печатает комбинированный вывод (как стандартный вывод, так и стандартную ошибку).
Наконец, он проверяет, была ли ошибка во время выполнения. Если произошла ошибка и флаг ignoreErrors имеет значение false, то вылетает multi-git. Причиной необязательного игнорирования ошибок является то, что иногда все в порядке, если команды не выполняются в некоторых репозиториях. Например, если вы хотите проверить ветку «Cool Feature» во всех репозиториях, в которых есть эта ветвь, вам все равно, если проверка не удалась в репозиториях, у которых нет этой ветки.
1 |
for _, r := range repos { |
2 |
|
3 |
// Go to the repo's directory
|
4 |
|
5 |
os.Chdir(r); |
6 |
|
7 |
|
8 |
|
9 |
// Print the command
|
10 |
|
11 |
fmt.Printf("[%s] %s\n", r, command_string) |
12 |
|
13 |
|
14 |
|
15 |
// Execute the command
|
16 |
|
17 |
out, err := exec.Command("git", git_components...).CombinedOutput() |
18 |
|
19 |
|
20 |
|
21 |
// Print the result
|
22 |
|
23 |
fmt.Println(string(out)) |
24 |
|
25 |
|
26 |
|
27 |
// Bail out if there was an error and NOT ignoring errors
|
28 |
|
29 |
if err != nil && !*ignoreErrors { |
30 |
|
31 |
os.Exit(1) |
32 |
|
33 |
}
|
34 |
|
35 |
}
|
36 |
|
37 |
|
38 |
|
39 |
fmt.Println("Done.") |
Заключение
Go - простой, но мощный язык. Он предназначен для крупномасштабного системного программирования, но отлично работает и для небольших программ командной строки. Минимальный дизайн Go резко контрастирует с другими современными языками, такими как Scale и Rust, которые также очень мощные и хорошо продуманные, но имеют очень крутую кривую обучения. Я призываю вас попробовать Go и экспериментировать. Это очень весело.



