Tuesday, September 17, 2024

[Golang] Phần 19 – Go Exec Command

-

1. Tổng quan.

Golang là một ngôn ngữ lập trình và os/exec là một package trong Golang được sử dụng để thực hiện các lệnh hệ thống bên ngoài. Bạn có thể gặp khó khăn khi sử dụng package này, đặc biệt là khi bạn là người mới bắt đầu.

Dưới đây là một số câu hỏi phổ biến và giải đáp chúng:

  • Làm thế nào để sử dụng os/exec với nhiều tham số?
    • Bạn có thể sử dụng hàm command để tạo một đối tượng Cmd, sau đó sử dụng phương thức Args để thiết lập danh sách các tham số.
  • Tại sao thư mục làm việc cwd của exec không phải là thư mục mà tôi đã gán?
    • Điều này có thể xảy ra nếu bạn không thiết lập đúng thư mục làm việc. Bạn cần sử dụng phương thức Dir trên đối tượng Cmd để đặt thư mục làm việc mong muốn.
  • Làm thế nào để chuyển đổi lệnh chuyển tiếp vào cuộc gọi hàm exec?
    • Bạn có thể sử dụng phương thức StdinPipe để tạo một đối tượng io.WriteCloser để viết dữ liệu vào tiến trình thực thi.

2. Một số ví dụ về cách sử dụng os/exec.

Dưới đây là một số ví dụ nhanh để minh họa cách sử dụng os/exec trong những tình huống phổ biến khi làm việc với các lệnh hệ thống hoặc các chương trình ngoại vi trong dự án của bạn.

Ví dụ 1 – Gọi hàm exec cơ bản.

package main

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

func main() {
	// Tạo một đối tượng Cmd để thực hiện lệnh "ls -lah"
	cmd := exec.Command("ls", "-lah")

	// Thực hiện lệnh và lấy kết quả đầu ra kết hợp (stdout và stderr)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Fatalf("cmd.Run() failed with %s\n", err)
	}

	// In kết quả ra màn hình
	fmt.Printf("combined out:\n%s\n", string(out))
}

Giải thích:

  • exec.Command("ls", "-lah"): Tạo một đối tượng Cmd để thực hiện lệnh “ls -lah”.
  • cmd.CombinedOutput(): Thực hiện lệnh và trả về kết quả đầu ra kết hợp (stdout và stderr).
  • Nếu có lỗi (err không nil) in thông báo lỗi và kết thúc chương trình.
  • In kết quả ra màn hình.

Ví dụ 2 – Gọi hàm exec với biến môi trường bổ sung.

package main

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

func main() {
	// Tạo một đối tượng Cmd để thực hiện một chương trình tùy ý
	cmd := exec.Command("programToExecute")

	// Tạo một biến môi trường bổ sung ("FOO=bar") và thêm vào danh sách môi trường hiện tại
	additionalEnv := "FOO=bar"
	newEnv := append(os.Environ(), additionalEnv)
	cmd.Env = newEnv

	// Thực hiện lệnh và lấy kết quả đầu ra kết hợp (stdout và stderr)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Fatalf("cmd.Run() failed with %s\n", err)
	}

	// In kết quả ra màn hình
	fmt.Printf("%s", out)
}

Giải thích:

  • exec.Command("programToExecute"): Tạo một đối tượng Cmd để thực hiện một chương trình tùy ý.
  • additionalEnv := "FOO=bar": Tạo một biến môi trường bổ sung “FOO=bar”.
  • newEnv := append(os.Environ(), additionalEnv): Thêm biến môi trường bổ sung vào danh sách môi trường hiện tại.
  • cmd.Env = newEnv: Đặt danh sách môi trường mới cho đối tượng Cmd.
  • Thực hiện lệnh và xử lý kết quả tương tự như trong ví dụ 1.

Cả hai ví dụ đều thực hiện lệnh và lấy kết quả đầu ra nhưng ví dụ thứ hai minh họa cách bạn có thể thêm biến môi trường bổ sung khi gọi lệnh.

Ví dụ 3 – Thiết lập thư mục làm việc (CWD) cho đường dẫn thực thi.

package main

import (
	"fmt"
	"log"
	"os/exec"
	"path/filepath"
)

func main() {
	// Cách 1: Thiết lập thư mục làm việc trực tiếp
	cmd := exec.Command("git", "log")
	cmd.Dir = "your/intended/working/directory"
	out, err := cmd.Output()

	if err != nil {
		log.Fatalf("cmd.Output() failed with %s\n", err)
	}

	fmt.Printf("%s", out)

	// Cách 2: Sử dụng đường dẫn tuyệt đối của binary
	abspath, _ := filepath.Abs("./mybinary")
	cmd = exec.Command(abspath, "log")
	cmd.Dir = "your/intended/working/directory"
	out, err = cmd.Output()

	if err != nil {
		log.Fatalf("cmd.Output() failed with %s\n", err)
	}

	fmt.Printf("%s", out)
}

Giải thích:

  • cmd.Dir = "your/intended/working/directory": Thiết lập thư mục làm việc cho đối tượng Cmd.

Ví dụ 4 – Sử dụng os/exec với lệnh bash trên Linux.

package main

import (
	"log"
	"os/exec"
)

func main() {
	rcmd := `iw dev | awk '$1=="Interface"{print $2}'`

	// Sử dụng "bash -c" để chạy lệnh bash
	cmd := exec.Command("bash", "-c", rcmd)

	// Lấy kết quả đầu ra kết hợp (stdout và stderr)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Println(err.Error())
	}

	log.Println(string(out))
}

Giải thích:

  • cmd := exec.Command("bash", "-c", rcmd): Tạo một đối tượng Cmd để chạy lệnh bash với nội dung được xác định bởi biến rcmd.
  • cmd.CombinedOutput(): Lấy kết quả đầu ra kết hợp (stdout và stderr) của lệnh bash.
  • Nếu có lỗi, in ra thông báo lỗi.

Ví dụ 5 – Sử dụng os/exec với lệnh Batch trên Windows.

package main

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

func main() {
	// Sử dụng "cmd /c" để chạy lệnh Batch trên Windows
	cmd := exec.Command("cmd", "/c", "ffmpeg -i myfile.mp4 myfile.mp3 && del myfile.mp4")

	// Lấy kết quả đầu ra kết hợp (stdout và stderr)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Println(err.Error())
	}

	log.Println(string(out))
}

Giải thích:

  • cmd := exec.Command("cmd", "/c", "ffmpeg -i myfile.mp4 myfile.mp3 && del myfile.mp4"): Tạo một đối tượng Cmd để chạy lệnh Batch với nội dung được xác định bởi chuỗi “ffmpeg -i myfile.mp4 myfile.mp3 && del myfile.mp4”.
  • cmd.CombinedOutput(): Lấy kết quả đầu ra kết hợp (stdout và stderr) của lệnh Batch.
  • Nếu có lỗi, in ra thông báo lỗi.

Ví dụ 6 – Sử dụng string slice khi gọi hàm os/exec.

package main

import (
	"fmt"
	"os/exec"
)

func main() {
	// Sử dụng slice chuỗi để truyền tham số
	args := []string{"hello", "world"}
	cmd := exec.Command("echo", args...)

	// Lấy kết quả đầu ra
	out, err := cmd.Output()
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(out))
}

Giải thích:

  • args := []string{"hello", "world"}: Tạo một slice chuỗi với các tham số bạn muốn truyền.
  • cmd := exec.Command("echo", args...): Tạo một đối tượng Cmd với lệnh “echo” và tham số từ slice chuỗi.
  • cmd.Output(): Lấy kết quả đầu ra của lệnh “echo” với các tham số đã được chỉ định.
  • Nếu có lỗi, in ra thông báo lỗi.

Ví dụ 7 – Giải quyết vấn đề về localization trong Windows CMD cho os/exec.

package main

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

func main() {
	// Sử dụng "cmd /c" để chạy lệnh Batch trên Windows
	cmd := exec.Command("cmd", "/c", "chcp 65001 && netsh WLAN show drivers")

	// Lấy kết quả đầu ra kết hợp (stdout và stderr)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Println(err.Error())
	}

	// Chuyển đổi kết quả thành chuỗi và tách dòng
	outputLines := strings.Split(string(out), "\n")

	// Lọc và in các dòng từ dòng thứ 2 trở đi (loại bỏ dòng xác nhận chcp)
	for i, line := range outputLines {
		if i > 0 {
			log.Println(line)
		}
	}
}

Giải thích:

  • cmd := exec.Command("cmd", "/c", "chcp 65001 && netsh WLAN show drivers"): Tạo một đối tượng Cmd để chạy lệnh Batch với nội dung được xác định bởi chuỗi “chcp 65001 && netsh WLAN show drivers”. chcp 65001 là lệnh để chuyển đổi mã codepage sang UTF-8.
  • cmd.CombinedOutput(): Lấy kết quả đầu ra kết hợp (stdout và stderr) của lệnh Batch.
  • strings.Split(string(out), "\n"): Chia kết quả thành các dòng riêng biệt.
  • for i, line := range outputLines: Duyệt qua từng dòng của kết quả.
  • if i > 0: Kiểm tra để đảm bảo rằng chỉ in các dòng từ dòng thứ hai trở đi (loại bỏ dòng xác nhận chcp).
  • log.Println(line): In các dòng sau khi đã xác định được mã codepage.

Ví dụ 7 – Ví dụ tổng hợp.

Ở ví dụ này mình sử dụng các module như sau:

encoding/json: Gói này cung cấp các chức năng để mã hóa và giải mã dữ liệu JSON trong Go. Trong mã của bạn, gói này được sử dụng để chuyển đổi slice của FileSystemInfo thành định dạng JSON thông qua hàm json.MarshalIndent. Nó giúp bạn có thể dễ dàng chuyển đổi giữa dữ liệu Go và dữ liệu JSON.

import "encoding/json"

fmt: Gói fmt cung cấp các chức năng để định dạng và in dữ liệu. Trong mã của bạn, fmt được sử dụng để in định dạng JSON vào màn hình.

import "fmt"

os/exec: Gói os/exec cung cấp các chức năng để thực hiện các lệnh hệ thống và tương tác với chúng. Trong mã của bạn, os/exec được sử dụng để chạy lệnh hệ thống df -h và thu thập đầu ra của nó.

import "os/exec"

strings: Gói strings cung cấp các chức năng để xử lý chuỗi. Trong mã của bạn, strings được sử dụng để tách và xử lý dữ liệu đầu ra từ lệnh df -h.

import "strings"

Tổng cộng, các gói này giúp bạn thực hiện các nhiệm vụ như thực thi lệnh hệ thống, xử lý và tách chuỗi, định dạng và in dữ liệu, cũng như chuyển đổi giữa dữ liệu Go và định dạng JSON.

Dưới đây là đoạn code sử dụng các module đã liệt kê ở trên, mình chạy lệnh df -h trong Linux bằng Golang, có thể sử dụng gói os/exec để thực hiện lệnh và thu thập kết quả, sau đó tách các giá trị từ đầu ra của lệnh df -h và chuyển chúng thành định dạng JSON, bạn có thể sử dụng các gói như stringsencoding/json.

package main

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

type FileSystemInfo struct {
	Filesystem string `json:"filesystem"`
	Size       string `json:"size"`
	Used       string `json:"used"`
	Avail      string `json:"avail"`
	UsePercent string `json:"use_percent"`
	MountedOn  string `json:"mounted_on"`
}

func main() {
	cmd := exec.Command("df", "-h")

	output, err := cmd.Output()
	if err != nil {
		fmt.Println("Lỗi khi chạy lệnh:", err)
		return
	}

	// Tách các dòng từ kết quả
	lines := strings.Split(string(output), "\n")

	// Kiểm tra nếu slice lines không chứa ít nhất 2 phần tử (tiêu đề và một dòng dữ liệu)
	if len(lines) < 2 {
		fmt.Println("Không có đầu ra hợp lệ từ lệnh df -h.")
		return
	}

	var fileSystems []FileSystemInfo

	// Bắt đầu từ dòng thứ hai để bỏ qua dòng tiêu đề
	for i := 1; i < len(lines); i++ {
		// Tách các trường từ dòng
		fields := strings.Fields(lines[i])

		// Kiểm tra xem dòng có đủ trường không
		if len(fields) >= 6 {
			fs := FileSystemInfo{
				Filesystem: fields[0],
				Size:       fields[1],
				Used:       fields[2],
				Avail:      fields[3],
				UsePercent: fields[4],
				MountedOn:  fields[5],
			}

			fileSystems = append(fileSystems, fs)
		} else {
			fmt.Println("Dòng không đủ trường:", lines[i])
		}
	}

	// Chuyển slice thành JSON
	jsonData, err := json.MarshalIndent(fileSystems, "", "  ")
	if err != nil {
		fmt.Println("Lỗi khi chuyển đổi thành JSON:", err)
		return
	}

	// In kết quả JSON
	fmt.Println(string(jsonData))
}

Câu lệnh var fileSystems []FileSystemInfo trong ngôn ngữ Go (Golang) đang khai báo một biến có tên là fileSystems là một slice (mảng động) chứa các phần tử thuộc kiểu dữ liệu FileSystemInfo. Trong ngữ cảnh này, FileSystemInfo là một kiểu dữ liệu được xác định ở nơi khác trong mã nguồn của bạn.

Nếu FileSystemInfo là một kiểu dữ liệu (struct) nào đó đã được định nghĩa trước đó trong mã nguồn của bạn, thì biến fileSystems sẽ là một slice chứa các phần tử thuộc kiểu dữ liệu đó.

var fileSystems []FileSystemInfo

Mình sử dụng gói strings để tách dòng và trường từ kết quả lệnh. Sau đó, mình sử dụng một struct FileSystemInfo để lưu trữ thông tin từng dòng và sử dụng gói encoding/json để chuyển đổi slice của các FileSystemInfo thành định dạng JSON.

Tiếp theo mình đã thêm các điều kiện kiểm tra trước khi truy cập các phần tử trong slice để tránh lỗi “index out of range”. Nếu một dòng không đủ trường, mình sẽ cho in thông báo lỗi và tiếp tục với dòng tiếp theo.

Kết quả của lệnh df -f của tôi khi gõ trong terminal là.

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            1.9G     0  1.9G   0% /dev
tmpfs           392M  1.1M  391M   1% /run
/dev/sda1        97G  4.1G   93G   5% /
tmpfs           2.0G     0  2.0G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           2.0G     0  2.0G   0% /sys/fs/cgroup
/dev/loop2       41M   41M     0 100% /snap/snapd/20290
/dev/loop1       92M   92M     0 100% /snap/lxd/24061
/dev/loop0       64M   64M     0 100% /snap/core20/2015
/dev/sda15      105M  6.1M   99M   6% /boot/efi
tmpfs           392M     0  392M   0% /run/user/0

Và sau khi dùng Golang để phân tích thành JSON.

$ go run main.go 
Dòng không đủ trường: 
[
  {
    "filesystem": "udev",
    "size": "1.9G",
    "used": "0",
    "avail": "1.9G",
    "use_percent": "0%",
    "mounted_on": "/dev"
  },
  {
    "filesystem": "tmpfs",
    "size": "392M",
    "used": "1.1M",
    "avail": "391M",
    "use_percent": "1%",
    "mounted_on": "/run"
  },
  {
    "filesystem": "/dev/sda1",
    "size": "97G",
    "used": "4.1G",
    "avail": "93G",
    "use_percent": "5%",
    "mounted_on": "/"
  },
  {
    "filesystem": "tmpfs",
    "size": "2.0G",
    "used": "0",
    "avail": "2.0G",
    "use_percent": "0%",
    "mounted_on": "/dev/shm"
  },
  {
    "filesystem": "tmpfs",
    "size": "5.0M",
    "used": "0",
    "avail": "5.0M",
    "use_percent": "0%",
    "mounted_on": "/run/lock"
  },
  {
    "filesystem": "tmpfs",
    "size": "2.0G",
    "used": "0",
    "avail": "2.0G",
    "use_percent": "0%",
    "mounted_on": "/sys/fs/cgroup"
  },
  {
    "filesystem": "/dev/loop2",
    "size": "41M",
    "used": "41M",
    "avail": "0",
    "use_percent": "100%",
    "mounted_on": "/snap/snapd/20290"
  },
  {
    "filesystem": "/dev/loop1",
    "size": "92M",
    "used": "92M",
    "avail": "0",
    "use_percent": "100%",
    "mounted_on": "/snap/lxd/24061"
  },
  {
    "filesystem": "/dev/loop0",
    "size": "64M",
    "used": "64M",
    "avail": "0",
    "use_percent": "100%",
    "mounted_on": "/snap/core20/2015"
  },
  {
    "filesystem": "/dev/sda15",
    "size": "105M",
    "used": "6.1M",
    "avail": "99M",
    "use_percent": "6%",
    "mounted_on": "/boot/efi"
  },
  {
    "filesystem": "tmpfs",
    "size": "392M",
    "used": "0",
    "avail": "392M",
    "use_percent": "0%",
    "mounted_on": "/run/user/0"
  }
]

Tham khảo nguồn https://dev.to/tobychui/quick-notes-for-go-os-exec-3ejg.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

4,956FansLike
256FollowersFollow
223SubscribersSubscribe
spot_img

Related Stories