---
id: linux-shell-scripting
title: "Shell scripting"
track: linux
trackTitle: "Linux Basics & Shell Scripting"
order: 5
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - linux-filesystem-permissions
  - linux-process-management
next: linux-ssh
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Bash Reference Manual — The Set Builtin"
    url: https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin
  - title: "Google Shell Style Guide"
    url: https://google.github.io/styleguide/shellguide.html
---

# Shell scripting

Shebang trong bài là Bash. Script có `[[ ]]` sẽ không chạy trên shell POSIX thuần như `dash`.

## Để làm gì

Bạn gom một chuỗi lệnh đã chạy ổn thành một file chạy lại được, và bắt script dừng ngay khi một lệnh bên trong fail. Với automation nhỏ trên một máy, script là đủ, chưa cần tới Ansible hay pipeline.

## Gồm những gì

- Shebang và bit execute.
- `set -euo pipefail`.
- Biến, quote, và `[[ ]]`.
- Argument: `$1`, `$#`, `"$@"`.
- Exit code.
- Một script lab, không cần root.

Khi script dài ra, có nhiều hàm và phải parse dữ liệu, hãy chuyển sang Python hoặc một tool quản lý config. Bash hợp nhất khi việc chính là gọi lệnh của hệ thống.

## Dùng khi nào

Dùng script cho thao tác lặp lại trên một máy Linux: tạo directory lab, kiểm tra một file, chạy backup, hoặc chuẩn bị một lệnh deploy.

Đừng dùng Bash để gọi API cloud có phân trang, parse JSON lớn, hay xử lý song song phức tạp. Ở những chỗ đó, word splitting và exit code rất dễ nuốt mất lỗi.

## Kiến thức cốt lõi

Dòng đầu `#!/usr/bin/env bash` nhờ `env` tìm Bash trong `PATH`. Kernel chỉ đọc shebang khi file được execute trực tiếp, ví dụ `./deploy.sh`.

Trong các script lab của lộ trình này, ba option sau nên đứng ngay sau shebang:

| Option | Tác dụng |
| --- | --- |
| `-e` | Thoát khi một lệnh trả exit code khác 0, trừ một số ngữ cảnh như nhánh `if`. |
| `-u` | Thoát khi đọc một biến chưa được gán. |
| `-o pipefail` | Pipeline fail nếu bất kỳ lệnh nào trong đó fail; exit code là của lệnh fail nằm bên phải nhất, thay vì luôn lấy của lệnh cuối. |

Không có `pipefail`, `false | true` vẫn tính là thành công vì `true` là lệnh cuối. Hệ quả là một pipeline kiểm tra log có thể báo thành công dù lệnh đầu tiên đã lỗi.

Hãy quote mọi biến bằng dấu nháy kép. Nếu đường dẫn có khoảng trắng, `$APP_DIR` sẽ bị tách thành nhiều từ, còn `"$APP_DIR"` vẫn giữ nguyên là một argument.

`[[ -d "$APP_DIR" ]]` là cách Bash kiểm tra một directory có tồn tại không. Bên trong `[[ ]]`, Bash không word-split biến như khi dùng `[`, nên ít bẫy hơn.

`$1` là argument thứ nhất. `$#` là số argument. `"$@"` trong dấu nháy kép giữ mỗi argument là một từ riêng. Không có nháy, `$@` bị tách giống biến thường. `"$*"` gộp mọi argument thành một từ, nối bằng ký tự đầu của `IFS`. Script nhận nhiều đường dẫn thì vòng lặp đi trên `"$@"`, không đi trên `"$*"`.

Exit code `0` là thành công, mọi giá trị khác là lỗi. Ở nhánh lỗi, hãy `exit 1` để chương trình gọi script biết kết quả.

`git -C "$APP_DIR"` chạy Git trong directory đó mà không đổi directory của chính script. Với `cd`, nếu lệnh `cd` fail thì lệnh sau vẫn có thể chạy ở directory cũ; `git -C` tránh được rủi ro đó.

## Lệnh và code mẫu

Tạo `$HOME/devops-lab/linux/check-dir.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

APP_DIR="${1:-$HOME/devops-lab/linux/app}"

if [[ -d "$APP_DIR" ]]; then
  printf 'Thu muc ton tai: %s\n' "$APP_DIR"
  find "$APP_DIR" -maxdepth 1 -type f -print
else
  printf 'Thu muc khong ton tai: %s\n' "$APP_DIR" >&2
  exit 1
fi
```

Khi sửa script này, giữ nguyên mấy điểm sau:

- Thông báo lỗi ghi ra stderr bằng `>&2`.
- Đường dẫn lấy từ argument thứ nhất và có giá trị mặc định.
- Mọi chỗ dùng biến đều có quote.

Nếu script thật sự cần cập nhật một Git checkout, hãy dùng:

```bash
git -C "$APP_DIR" pull --ff-only origin main
```

Với `--ff-only`, lệnh sẽ fail khi history không fast-forward được, thay vì tự tạo một merge commit.

## Thực hành

```bash
mkdir -p "$HOME/devops-lab/linux/app"
printf 'demo\n' > "$HOME/devops-lab/linux/app/README.txt"
chmod 755 "$HOME/devops-lab/linux/check-dir.sh"

"$HOME/devops-lab/linux/check-dir.sh"
"$HOME/devops-lab/linux/check-dir.sh" "$HOME/devops-lab/linux/missing"
echo "exit code: $?"
```

## Kết quả mong đợi

- Lần chạy đầu in đường dẫn directory `app` và file `README.txt`, exit code `0`.
- Lần chạy với directory thiếu in thông báo ra stderr và exit code `1`.
- Nếu bỏ quote trong một bản copy rồi truyền đường dẫn có khoảng trắng, bạn sẽ thấy lệnh nhận nhiều argument thay vì một. Thử xong thì trả quote về như mẫu.

## Checklist

- [ ] Gắn shebang và chạy bằng bit execute, đừng dùng `bash script.sh` để che shebang sai.
- [ ] Đặt `set -euo pipefail` trước phần chính của script.
- [ ] Quote mọi biến trong lệnh bằng dấu nháy kép.
- [ ] Trả exit code khác 0 và ghi ra stderr ở nhánh lỗi.
- [ ] Đọc một argument bằng `"$1"`. Nhiều argument thì dùng `"$@"`.

## Thẻ ôn

### linux-shell-scripting-shebang
kind: command
cue: Shebang chỉ được kernel đọc khi bạn chạy file trực tiếp, ví dụ `./deploy.sh`. `bash script.sh` bỏ qua shebang sai, nên đừng dùng cách đó để kiểm tra.

Q: Dòng đầu script là `#!/usr/bin/env bash`. Bạn muốn chắc kernel thật sự dùng dòng đó để chọn trình thông dịch. Chạy `bash script.sh` có chứng minh được không, và cách chạy nào mới đọc shebang?

A: Kernel chỉ nhìn shebang khi bạn thực thi file như một chương trình, ví dụ `./deploy.sh` sau khi file có quyền chạy. Lúc đó nó đọc dòng `#!` và gọi đúng trình thông dịch. `bash script.sh` là bạn tự chọn bash và đưa file cho bash đọc. Shebang sai cũng bị bỏ qua, script vẫn chạy, và bạn tưởng dòng đầu đã đúng. Muốn kiểm tra shebang, chạy trực tiếp file đó.

### linux-shell-scripting-set
kind: command
cue: Ngay sau shebang nên có `set -euo pipefail`. Thiếu `pipefail` thì `false | true` vẫn bị tính là thành công.

Q: Một script deploy chạy tiếp dù một lệnh ở giữa đã lỗi, và một pipeline `lệnh-fail | lệnh-ok` vẫn có exit code 0. Ba option nào nên đứng ngay sau shebang, mỗi option chặn kiểu lỗi nào?

A: `set -euo pipefail`. `-e` làm script thoát khi một lệnh trả mã khác 0, thay vì chạy tiếp trên trạng thái đã hỏng. `-u` thoát khi đọc một biến chưa được gán, để một tên biến gõ nhầm không thành chuỗi rỗng âm thầm. `-o pipefail` làm cả pipeline thất bại nếu một lệnh trong pipe thất bại. Không có `pipefail`, exit code của pipeline là exit code của lệnh cuối, nên `false | true` vẫn là thành công và script tưởng bước đó đã xong.

### linux-shell-scripting-quote
kind: trap
cue: Biến có khoảng trắng mà không có dấu nháy sẽ bị shell tách thành nhiều argument. Trong lệnh, viết `"$APP_DIR"`.

Q: `APP_DIR` đang là một đường dẫn có khoảng trắng, ví dụ một thư mục tên có dấu cách. Script gọi lệnh với `$APP_DIR` không bọc nháy. Shell làm gì với giá trị đó?

A: Shell khai triển biến rồi tách theo khoảng trắng thành nhiều từ, mỗi từ một argument. Lệnh nhận một đường dẫn gãy làm hai, và thường báo không thấy file, hoặc tệ hơn là đụng một path không định đụng. `"$APP_DIR"` giữ nguyên một argument, khoảng trắng nằm trong giá trị. Quote mọi biến đưa vào lệnh bằng dấu nháy kép, kể cả khi hôm nay giá trị chưa có dấu cách.

### linux-shell-scripting-curl-bash
kind: trap
cue: `curl ... | bash` chạy ngay nội dung vừa tải, không có bước đọc. Tải file, đọc, rồi mới chạy bản đã kiểm tra.

Q: Hướng dẫn cài một tool bảo chạy `curl` một URL rồi pipe thẳng vào `bash`. Trên máy sẽ dùng lâu, có nên làm theo không, và cách làm có bước kiểm tra là gì?

A: Không. Pipe nối stdout của `curl` vào stdin của bash, nên bash chạy từng phần ngay khi tải về. Không có file để bạn mở ra đọc trước, và một nội dung đổi trên server là lần chạy sau đã khác lần bạn tưởng mình đã xem. Tải thành một file, đọc file đó, rồi mới chạy đúng bản đã kiểm tra. Bước đọc là việc không thể có khi pipe thẳng vào shell.

### linux-shell-scripting-args
kind: trap
cue: Một argument là `"$1"`. Nhiều argument giữ nguyên từng từ thì `"$@"`. `"$*"` gộp tất cả thành một từ.

Q: Script nhận vài đường dẫn, có đường dẫn chứa khoảng trắng. Vòng lặp viết `for dir in $*` hoặc `for dir in "$*"`. Shell đưa vào vòng lặp cái gì, và dạng nào giữ mỗi đường dẫn là một argument?

A: Không nháy thì `$*` và `$@` đều bị tách theo khoảng trắng, một đường dẫn thành hai vòng. `"$*"` gộp mọi argument thành một từ, vòng lặp chỉ chạy một lần với cả chuỗi. `"$@"` mới giữ mỗi argument là một từ, khoảng trắng nằm trong từ đó. `$#` cho biết có bao nhiêu argument, không thay cho danh sách.

## Lỗi thường gặp

**Chỉ bật `set -e`.** Pipeline vẫn che lỗi của các lệnh phía trước, nên cần thêm `-o pipefail`. Thêm cả `-u` để khi gõ sai tên biến, script fail ngay thay vì âm thầm dùng chuỗi rỗng.

**`cd "$APP_DIR" && git pull` trong script dài.** Nếu sau này ai đó lỡ xóa `&&`, Git sẽ chạy ở directory hiện tại của process. `git -C` gắn directory vào đúng lệnh cần nó.

**Tin `curl ... | bash`.** Pipe này chạy luôn nội dung vừa tải về mà không qua bước review nào. Hãy tải file, đọc file, rồi mới chạy bản đã kiểm tra.

**Vòng lặp trên `"$*"` khi script nhận nhiều đường dẫn.** Cả danh sách thành một argument. Dùng `"$@"`.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày Bash script. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [Bash Reference Manual — The Set Builtin](https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin) — GNU, semantics của `-e`, `-u`, và `pipefail`.
3. [Bash Reference Manual — Special Parameters](https://www.gnu.org/software/bash/manual/bash.html#Special-Parameters) — `$1`, `$#`, `$@`, `$*`.
4. [Bash Reference Manual — Conditional Constructs](https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs) — `[[ ]]`.
5. [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html) — quy ước quote và `set` cho script giữ được lâu.
6. [git pull](https://git-scm.com/docs/git-pull) — `--ff-only` trong tài liệu Git.

## Tiếp theo

[SSH](./04-ssh.md)
