Go programından dış bir komut çalıştırmak için os/exec paketini kullanırsınız: exec.Command("program", "arg1", "arg2") bir *exec.Cmd hazırlar, Output() çalıştırıp standart çıktıyı döndürür, Run() yalnızca bitmesini bekler, Start() ve Wait() ise başlatma ile beklemeyi ayırır. Komut sıfır olmayan bir kodla biterse hata *exec.ExitError tipindedir; program hiç bulunamazsa süreç başlamaz ve hata exec.ErrNotFound değerini sarar. Paket, C'deki system çağrısının aksine kabuğu çağırmaz.

Bu yazıda komut çalıştırmanın farklı yollarını, çıktı ve hata akışlarını yakalamayı, standart girdiye veri vermeyi, çıkış kodunu okumayı, CommandContext ile zaman aşımını ve os/signal paketiyle Ctrl+C geldiğinde düzgün kapanmayı anlatıyoruz. Örneklerin hem Windows hem Linux'ta aynı sonucu vermesi için "dış komut" olarak programın kendisini bir ortam değişkeniyle çocuk modunda başlatıyoruz; gerçek kodda bu yerde git, ffmpeg ya da başka bir program olur. Örnekler Go 1.27.3 ile çalıştırıldı.

Command, Output ve argümanlar

Command, Output ve argümanlar: Run(), Output(), CombinedOutput(), Start() + Wait()

exec.Command yalnızca Path ve Args alanlarını doldurur; komut adında yol ayırıcı yoksa programı LookPath ile PATH içinde arar. Args[0] her zaman verdiğiniz ad olur, bu yüzden argümanlara komut adını tekrar yazmazsınız. Belgede en çok vurgulanan nokta, paketin kabuk çağırmamasıdır: glob desenleri (*.txt) genişletilmez, boru hattı, yönlendirme ve ortam değişkeni genişletmesi yapılmaz. Her argüman olduğu gibi tek bir argüman olarak iletilir; bu, kullanıcı girdisini kabuk komutuna gömmekten doğan enjeksiyon riskini ortadan kaldırır.

go
package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
)

// Örnek her işletim sisteminde çalışsın diye "dış komut" olarak programın
// kendisini çocuk modunda başlatıyoruz. Gerçek kodda burada git, ffmpeg vb. olur.
func cocuk() {
	fmt.Printf("çocuk argümanları: %q\n", os.Args[1:])
	fmt.Println("ORTAM =", os.Getenv("ORTAM"))
	os.Exit(0)
}

func main() {
	if os.Getenv("COCUK_MOD") == "1" {
		cocuk()
	}
	exe, err := os.Executable()
	if err != nil {
		fmt.Println(err)
		return
	}

	cmd := exec.Command(exe, "bir", "iki boşluklu", "*.txt")
	cmd.Env = append(os.Environ(), "COCUK_MOD=1", "ORTAM=test")

	out, err := cmd.Output() // çalıştır, bitmesini bekle, stdout'u döndür
	if err != nil {
		fmt.Println("hata:", err)
		return
	}
	fmt.Print(string(out))
	fmt.Println("başarılı mı:", cmd.ProcessState.Success(), "çıkış kodu:", cmd.ProcessState.ExitCode())
	fmt.Println(strings.Count(string(out), "\n"), "satır okundu")
}

// Çıktı:
// çocuk argümanları: ["bir" "iki boşluklu" "*.txt"]
// ORTAM = test
// başarılı mı: true çıkış kodu: 0
// 2 satır okundu

Çocuk süreç "iki boşluklu" değerini tek bir argüman olarak, *.txt değerini de genişletilmemiş haliyle aldı. cmd.Env alanını nil bırakırsanız çocuk süreç mevcut sürecin ortamını kullanır; burada olduğu gibi os.Environ() sonuna ekleme yaparak değişken eklersiniz. Belgeye göre Env içinde aynı anahtar birden çok kez geçerse son değer kullanılır. Çalışma dizini için Dir alanı vardır. Glob genişletmesi gerekiyorsa path/filepath.Glob, ortam değişkeni genişletmesi gerekiyorsa os.ExpandEnv kullanmanız önerilir. Örnekteki ProcessState alanı, süreç başarıyla başlatıldıysa Run ya da Wait tamamlandığında doldurulur; Success() ve ExitCode() bilgisini oradan okursunuz. os.Executable belgesine göre dönen yol hata yoksa mutlaktır, ama çalıştırılabilir dosyanın hâlâ o yolda olduğu garanti edilmez.

MetotNe yaparNot
Run()Başlatır ve bitmesini beklerStdout/Stderr nil ise çıktı null aygıtına gider
Output()Çalıştırır, stdout'u döndürürHata ExitError ise ve Stderr nil ise stderr'in bir kısmını hataya koyar
CombinedOutput()stdout ve stderr'i birlikte döndürürİki akış tek dilimde karışık gelir
Start() + Wait()Başlatır; beklemeyi siz yaparsınızStart başarılıysa Wait mutlaka çağrılmalı
StdoutPipe() vb.Akışı okuma veya yazma için boru verirTüm okumalar bitmeden Wait çağrılmamalı; Run ile kullanılmaz

Bir Cmd değeri Start, Run, Output ya da CombinedOutput çağrısından sonra tekrar kullanılamaz; aynı komutu yeniden çalıştırmak için yeni bir exec.Command oluşturursunuz.

Çıkış kodu, ExitError ve ErrNotFound

Bir dış komutun başarısız olmasının iki farklı anlamı vardır ve bunları ayırmanız gerekir. Süreç başladı ama sıfır olmayan bir kodla bittiyse hata *exec.ExitError olur; gömülü *os.ProcessState sayesinde ExitCode() metoduna erişirsiniz. Output kullandıysanız ve cmd.Stderr alanını ayarlamadıysanız, belgeye göre standart hatanın bir bölümü ExitError.Stderr alanına konur; çok uzunsa yalnızca başı ve sonu tutulur. Program hiç bulunamadıysa ise süreç başlamaz ve hata exec.ErrNotFound değerini sarar.

go
package main

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
)

func main() {
	if os.Getenv("COCUK_MOD") == "1" {
		fmt.Println("yarım iş")
		fmt.Fprintln(os.Stderr, "yapılandırma dosyası bulunamadı")
		os.Exit(3)
	}
	exe, _ := os.Executable()

	// 1) Çocuk süreç sıfır olmayan kodla çıkarsa: *exec.ExitError
	cmd := exec.Command(exe)
	cmd.Env = append(os.Environ(), "COCUK_MOD=1")
	out, err := cmd.Output()
	fmt.Printf("stdout: %q\n", out)

	var exitErr *exec.ExitError
	if errors.As(err, &exitErr) {
		fmt.Println("hata:", err)
		fmt.Println("kod:", exitErr.ExitCode())
		fmt.Printf("stderr: %q\n", exitErr.Stderr) // Output, Stderr nil ise doldurur
	}

	// 2) Program hiç bulunamazsa: süreç başlamaz, hata farklıdır.
	_, err = exec.Command("boyle-bir-program-yok").Output()
	fmt.Println("bulunamadı mı:", errors.Is(err, exec.ErrNotFound))
	fmt.Println("ExitError mı:", errors.As(err, &exitErr))
}

// Çıktı:
// stdout: "yarım iş\n"
// hata: exit status 3
// kod: 3
// stderr: "yapılandırma dosyası bulunamadı\n"
// bulunamadı mı: true
// ExitError mı: false

Bulunamayan program için err.Error() metni bu Windows makinesinde exec: "boyle-bir-program-yok": executable file not found in %PATH% şeklindeydi. Belgede ErrNotFound değerinin metni Unix sistemlerinde $PATH, Plan 9'da $path geçecek şekilde tanımlanmıştır; bu yüzden metni karşılaştırmak yerine errors.Is(err, exec.ErrNotFound) kullanmak taşınabilir tek yoldur. ProcessState.ExitCode belgesine göre süreç henüz bitmediyse ya da bir sinyalle sonlandırıldıysa değer -1 olur. Hata sarmalama ve errors.As için Go hata yönetimi yazısına bakın.

Geçerli dizindeki programlar ve ErrDot

Geçerli dizindeki programlar ve ErrDot

Belgeye göre Go 1.19'dan beri Command ve LookPath, PATH içindeki göreli bir girdi (örneğin .) yüzünden geçerli dizinde bulunan bir programı çözümlemez. LookPath("go"), PATH nasıl ayarlanmış olursa olsun Unix'te ./go, Windows'ta .\go.exe sonucunu döndürmez; bunun yerine errors.Is(err, exec.ErrDot) ile yakalanan bir hata verir. Gerçekten geçerli dizindeki programı çalıştırmak istiyorsanız adı açıkça ./prog şeklinde yazarsınız. Bu değişikliğin güvenlik gerekçesi Go Blog: Command PATH security in Go yazısında anlatılır. Windows tarafında Go 1.22 sürüm notları, LookPath fonksiyonunun %PATH% içindeki boş girdileri yok saydığını belirtir.

Standart girdi, ayrı çıktı tamponları ve zaman aşımı

cmd.Stdin alanına herhangi bir io.Reader, cmd.Stdout ve cmd.Stderr alanlarına herhangi bir io.Writer verebilirsiniz. Değer bir *os.File ise süreç o dosyaya doğrudan bağlanır; değilse paket arka planda bir goroutine ile veriyi boru üzerinden taşır ve Wait bu kopyalama bitmeden dönmez. Uzun sürebilecek komutlar için exec.CommandContext kullanırsınız: bağlam süre dolduğu ya da iptal edildiği için biterse komut kendiliğinden bitmeden önce cmd.Cancel çağrılır; varsayılan Cancel sürecin Kill metodunu çağırır.

go
package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
	"time"
)

func main() {
	switch os.Getenv("COCUK_MOD") {
	case "buyut": // stdin'i okuyup büyük harfe çevirir
		veri, _ := io.ReadAll(os.Stdin)
		fmt.Print(strings.ToUpper(string(veri)))
		return
	case "uyu": // hiç bitmeyecekmiş gibi bekler
		time.Sleep(10 * time.Second)
		return
	}
	exe, _ := os.Executable()

	// Stdin'e veri ver, Stdout ve Stderr'i ayrı tamponlara al.
	cmd := exec.Command(exe)
	cmd.Env = append(os.Environ(), "COCUK_MOD=buyut")
	cmd.Stdin = strings.NewReader("merhaba go\n")
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		fmt.Println("hata:", err, stderr.String())
	}
	fmt.Print(stdout.String())

	// Zaman aşımı: bağlam bitince süreç öldürülür.
	ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
	defer cancel()
	cmd = exec.CommandContext(ctx, exe)
	cmd.Env = append(os.Environ(), "COCUK_MOD=uyu")
	basla := time.Now()
	err := cmd.Run()
	fmt.Println("hata var mı:", err != nil)
	fmt.Println("bağlam:", ctx.Err(), errors.Is(ctx.Err(), context.DeadlineExceeded))
	fmt.Println("2 sn'den kısa sürdü mü:", time.Since(basla) < 2*time.Second)
}

// Çıktı:
// MERHABA GO
// hata var mı: true
// bağlam: context deadline exceeded true
// 2 sn'den kısa sürdü mü: true

İkinci bölümde çocuk süreç 10 saniye uyumak üzere başlatıldı, fakat bağlam 300 milisaniyede dolduğu için öldürüldü. Burada err değerinin metnini yazdırmadık, çünkü öldürülen bir sürecin raporu işletim sistemine göre değişir; bunun yerine bağlamın kendi hatasını (context deadline exceeded) kontrol ettik. Bağlamlar için Go context paketi yazısına bakın.

Cmd yapısındaki WaitDelay alanı iki gecikme kaynağını sınırlar: bağlam iptal edildikten sonra çıkmayan bir çocuk süreç ve çıkmış ama G/Ç borularını açık bırakmış bir süreç. Varsayılan sıfır değerinde borular EOF gelene kadar okunur; bu da komutun başlattığı torun süreçler boruları açık tuttuğu sürece Wait çağrısının beklemesine yol açabilir. Kendi iptal davranışınızı (örneğin önce kibar bir kapanma isteği göndermek) Cancel alanına bir fonksiyon atayarak tanımlayabilirsiniz.

Çıktıyı akış halinde okumak ve hata ayıklama

Output ve CombinedOutput, komut bitene kadar tüm çıktıyı bellekte biriktirir. Uzun süren ya da çok çıktı üreten komutlarda çıktıyı geldikçe işlemek istersiniz. Bunun için cmd.StdoutPipe() bir io.ReadCloser döndürür; Start ile komutu başlatır, okuyucuyu sonuna kadar tüketir ve en son Wait çağırırsınız. Belgedeki örnekte bu okuyucu doğrudan json.NewDecoder ile çözülür. Sıralama kritiktir: Wait süreç çıktığında boruyu kapattığı için okumalar bitmeden Wait çağırmak veri kaybına yol açar. StdinPipe ise tersi yönde çalışır; komut standart girdi kapanana kadar çıkmıyorsa boruyu sizin kapatmanız gerekir.

Hata ayıklarken cmd.String() komutun okunabilir bir tanımını verir; belgeye göre yalnızca hata ayıklama içindir, kabuğa girdi olarak verilmeye uygun değildir ve biçimi Go sürümleri arasında değişebilir. cmd.Environ(), komutun mevcut yapılandırmayla hangi ortamda çalışacağının bir kopyasını döndürür. Dir alanını Environ çağrısından önce ayarlarsanız, kullanılan platformlarda PWD değişkeni de güncellenmiş olarak gelir.

os/signal ile Ctrl+C ve zarif kapanış

Bir sunucu ya da uzun süren bir araç, kullanıcı Ctrl+C'ye bastığında ya da servis yöneticisi SIGTERM gönderdiğinde açık işleri bitirip kaynaklarını kapatmak ister. os/signal belgesine göre bir Go programında SIGHUP, SIGINT ve SIGTERM varsayılan olarak programı sonlandırır. signal.Notify bu varsayılan davranışı kapatıp sinyali bir kanala iletir; paket kanala gönderirken beklemediği için kanalın tamponlu olması gerekir, tek bir sinyal için 1 boyutu yeterlidir. Daha pratik olan signal.NotifyContext ise sinyal geldiğinde iptal edilen bir bağlam döndürür.

go
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"time"
)

func isle(ctx context.Context, n int) {
	for i := 1; i <= n; i++ {
		select {
		case <-ctx.Done():
			fmt.Println("iş yarıda bırakıldı:", context.Cause(ctx))
			return
		case <-time.After(100 * time.Millisecond):
			fmt.Println("adım", i)
		}
	}
}

func main() {
	// Ctrl+C (os.Interrupt) gelince ctx iptal olur; program hemen ölmez.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	// Örnek kendi kendine bitsin diye sinyal yerine 250 ms sonra stop() çağırıyoruz.
	// Gerçek programda bu satır olmaz; kullanıcı Ctrl+C'ye basar.
	time.AfterFunc(250*time.Millisecond, stop)

	isle(ctx, 10)
	fmt.Println("temizlik yapıldı, çıkılıyor")
}

// Çıktı:
// adım 1
// adım 2
// iş yarıda bırakıldı: context canceled
// temizlik yapıldı, çıkılıyor

Örneğin kendi kendine bitmesi için sinyal yerine 250 milisaniye sonra stop() çağırdık, bu yüzden context.Cause sonucu context canceled oldu. Go 1.26 sürüm notlarına göre gerçek bir sinyal bağlamı iptal ettiğinde NotifyContext artık bağlamı hangi sinyalin geldiğini belirten bir hatayla iptal eder; context.Cause bu durumda sinyali anlatan hatayı döndürür. stop fonksiyonu sinyal yönlendirmesini kaldırır ve varsayılan davranışı geri getirir; belgeler iş biter bitmez çağrılmasını önerir. Bu bağlamı Go web sunucusu yazısındaki Shutdown çağrısına bağlayarak zarif kapanış elde edersiniz.

Kanal tabanlı signal.Notify hâlâ kullanışlıdır: örneğin SIGHUP geldiğinde yapılandırmayı yeniden yüklemek gibi programı sonlandırmayan, tekrar eden olaylar için bir goroutine içinde kanalı döngüyle dinlersiniz. Aynı kanalla Notify birden çok kez çağrılırsa sinyal kümesi genişler; kümeden sinyal çıkarmanın tek yolu signal.Stop çağırmaktır. Belgeye göre SIGKILL ve SIGSTOP bir program tarafından yakalanamaz; bu yüzden zorla öldürülen bir süreçte temizlik kodu hiç çalışmaz ve kritik veriyi kapanışa bırakmamak gerekir.

Sık yapılan hatalar

Sık yapılan hatalar: Kabuk söz dizimi beklemek, Komut adını argümanlara tekrar yazmak, Start sonrası Wait çağırmamak, StdoutPipe ile Run kullanmak
  • Kabuk söz dizimi beklemek: exec.Command("ls *.go | wc -l") çalışmaz; paket kabuk çağırmaz. Gerçekten kabuk gerekiyorsa kabuğu açıkça çağırın ve kullanıcı girdisini çok dikkatli kaçırın; mümkünse borular yerine Go kodu yazın.
  • Komut adını argümanlara tekrar yazmak: exec.Command("git", "git", "status") git'e fazladan bir argüman verir.
  • Start sonrası Wait çağırmamak: belgeye göre başarılı bir Start sonrası sistem kaynaklarını serbest bırakmak için Wait zorunludur.
  • StdoutPipe ile Run kullanmak: Wait boruyu kapattığı için tüm okumalar bitmeden Wait çağrılmamalıdır; aynı nedenle borularla Run kullanmak hatalıdır.
  • Hata metnini karşılaştırmak: mesajlar işletim sistemine göre değişir; errors.Is ve errors.As kullanın.
  • Tamponsuz sinyal kanalı: signal.Notify kanala göndermek için beklemez; tamponsuz kanalda sinyal kaçabilir.
  • Aynı Cmd değerini tekrar çalıştırmak: her çalıştırma için yeni bir Cmd gerekir.

Sık Sorulan Sorular

Komutun çıktısını satır satır, canlı olarak nasıl okurum?

cmd.StdoutPipe() ile bir okuyucu alın, cmd.Start() çağırın, okuyucuyu bufio.Scanner ile satır satır okuyun ve okuma bitince cmd.Wait() çağırın. bufio.Scanner kullanımı için Go dosya okuma ve yazma yazısına bakın.

Windows'ta argümanlar nasıl iletilir?

Belgeye göre Windows'ta süreçler komut satırının tamamını tek bir string olarak alır ve kendileri ayrıştırır. Command, argümanları CommandLineToArgvW ile uyumlu bir algoritmayla birleştirip tırnaklar. msiexec.exe ve cmd.exe (dolayısıyla tüm toplu iş dosyaları) farklı kurallar kullandığı için bu durumlarda komut satırını SysProcAttr.CmdLine ile kendiniz verebilirsiniz.

Kullanıcı girdisini argüman olarak vermek güvenli mi?

Kabuk kullanılmadığı için girdi, kabuk meta karakterleri yorumlanmadan tek bir argüman olarak gider; bu enjeksiyon riskini büyük ölçüde azaltır. Yine de çalıştırdığınız programın kendisi argümanı bir seçenek gibi yorumlayabilir (örneğin - ile başlayan değerler); girdiyi doğrulamak sizin sorumluluğunuzdur.

Ctrl+C'den sonra ikinci kez basılınca program hemen kapanır mı?

NotifyContext belgesine göre dönen stop fonksiyonu çağrılana kadar sonraki kesme sinyalleri varsayılan çıkış davranışını tetiklemez. İlk sinyalden sonra stop() çağırırsanız ikinci Ctrl+C programı varsayılan şekilde sonlandırır; bu, takılan bir kapanışta kullanıcıya çıkış yolu bırakır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Go öğrenme rehberi · Go flag paketi · Go context paketi · Go net/http web sunucusu · Go hata yönetimi · Linux nedir