Biến môi trường và PATH: vì sao máy báo command not found

Cài xong một công cụ, gõ tên nó, và máy trả về command not found — dù file thực thi rõ ràng đang nằm trong ổ đĩa. Hoặc lệnh chạy tốt trên terminal nhưng vào script của cron thì chết. Gần như mọi trường hợp như vậy đều quy về hai thứ: biến môi trường và biến PATH.

Biến của shell khác biến môi trường

Gán một biến trong shell chỉ tạo ra biến cục bộ, tiến trình con không nhìn thấy nó. export mới là thứ đẩy biến vào môi trường được kế thừa:

TEN=xinchao
bash -c 'echo "[$TEN]"'        # in ra [] - tien trinh con khong thay

export TEN
bash -c 'echo "[$TEN]"'        # in ra [xinchao]

Xem và dọn:

printenv                  # chi cac bien moi truong
printenv PATH
env                       # tuong duong
set | less                # ca bien shell lan ham, danh sach rat dai
unset TEN                 # xoa han
export -n TEN             # giu bien nhung thoi xuat ra moi truong

Muốn đặt biến cho đúng một lệnh, không làm bẩn shell hiện tại:

LANG=C ls -l                       # chi cho lenh nay
env TZ=UTC date                    # cach viet ro rang hon

PATH là một danh sách, tìm từ trái sang phải

echo "$PATH"
printenv PATH | tr ':' '\n'        # de doc hon nhieu

Khi bạn gõ một tên lệnh không chứa dấu /, shell duyệt lần lượt từng thư mục trong PATH theo đúng thứ tự và dừng ở kết quả khớp đầu tiên. Không tìm thấy ở đâu cả thì mới báo command not found.

Thứ tự quyết định phiên bản nào thắng. Đây là lý do /usr/local/bin thường đứng trước /usr/bin: thứ bạn tự cài sẽ che thứ đi kèm hệ điều hành.

Lưu ý thư mục hiện tại không nằm trong PATH, và đó là chủ ý về bảo mật — nếu có, chỉ cần đặt một file tên ls trong thư mục dùng chung là bẫy được người khác. Muốn chạy script ở ngay đây thì phải nói rõ:

./caidat.sh              # dung
caidat.sh                # command not found, du file nam ngay day

Lệnh nào đang thực sự chạy

type -a python3          # liet ke TAT CA cho tim thay, theo thu tu
command -v python3       # duong dan dau tien, hop cho script
which python3            # chi tim file thuc thi

type là công cụ đáng tin nhất vì nó biết cả alias, hàm shell và lệnh dựng sẵn — những thứ which hoàn toàn không thấy. Khi một lệnh cư xử kỳ lạ, type -a thường trả lời ngay: hóa ra nó là một alias, hoặc có hai bản cài đè nhau.

Khi shell nhớ nhầm vị trí cũ

Bash và zsh ghi nhớ vị trí các lệnh đã chạy để khỏi phải dò lại PATH mỗi lần. Sau khi bạn di chuyển hoặc nâng cấp một chương trình, bộ nhớ đệm này thành sai — lệnh báo không tìm thấy file dù nó vừa được cài lại.

hash -r        # bash: xoa bo nho dem
rehash         # zsh
type -a lenh   # kiem tra lai

Thêm đường dẫn đúng cách

# DUNG - noi them vao dau, giu nguyen phan cu
export PATH="$HOME/.local/bin:$PATH"

# DUNG - noi vao cuoi, uu tien thap hon ban he thong
export PATH="$PATH:/opt/congcu/bin"

# SAI - xoa sach PATH, ca shell nay hong ngay lap tuc
export PATH=/opt/congcu/bin

Lỡ tay chạy dòng cuối thì đừng hoảng, cũng đừng đóng cửa sổ. Dựng lại một PATH tối thiểu là xong:

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Một lỗi nữa hay gặp là nối thêm mãi trong file cấu hình được nạp nhiều lần, khiến PATH dài ra sau mỗi lần mở shell lồng nhau. Kiểm tra bằng printenv PATH | tr ':' '\n' | sort | uniq -d.

File nào được nạp, vào lúc nào

Đây là phần gây nhầm lẫn nhiều nhất. Bash phân biệt ba tình huống:

  • Login shell (đăng nhập qua SSH, qua console): đọc /etc/profile, rồi đọc file đầu tiên tồn tại trong ba file ~/.bash_profile, ~/.bash_login, ~/.profile — các file còn lại bị bỏ qua hoàn toàn.
  • Tương tác nhưng không login (mở thêm tab terminal): đọc ~/.bashrc và file bashrc chung của hệ thống.
  • Không tương tác (chạy script, ssh may 'lenh'): không đọc file nào trong số trên.

Muốn biết mình đang ở tình huống nào:

shopt -q login_shell && echo "login shell" || echo "khong phai login shell"
echo "$-"        # co chu 'i' nghia la dang tuong tac

Với zsh thì cách chia khác hẳn: ~/.zshenv được nạp trong mọi trường hợp, ~/.zprofile chỉ khi login, ~/.zshrc chỉ khi tương tác, ~/.zlogin sau cùng của phiên login. Đặt PATH vào ~/.zshenv là cách chắc chắn nhất để script cũng thấy.

Vì sao sửa .bashrc mà chạy qua ssh vẫn không thấy

ssh may1 'echo $PATH'           # thuong KHONG doc .bashrc
ssh may1                        # login shell, co doc .profile

Chạy lệnh trực tiếp qua SSH tạo ra một shell không tương tác. Trên Debian và Ubuntu, ~/.bashrc mặc định còn mở đầu bằng một đoạn thoát ra ngay khi phát hiện không tương tác, nên dù có được nạp cũng không chạy tới phần bạn vừa thêm. Cấu hình cho các phiên đăng nhập nên đặt ở ~/.profile; và nếu công cụ tự động cần biến nào, hãy khai báo nó ngay trong script hoặc trong unit của systemd thay vì trông chờ vào profile.

Tiến trình con không sửa được môi trường của cha

Môi trường chỉ truyền một chiều: cha sang con. Con làm gì với bản sao của nó cũng không ảnh hưởng ngược lên cha. Hệ quả rất thực tế:

# caidat-bien.sh chua: export DUAN=/srv/duan

./caidat-bien.sh        # chay trong tien trinh con, mat ngay khi xong
echo "$DUAN"            # rong

source ./caidat-bien.sh # chay TRONG shell hien tai
. ./caidat-bien.sh      # cach viet ngan, y het
echo "$DUAN"            # /srv/duan

Cũng vì lẽ đó, một script không thể đổi thư mục làm việc của shell gọi nó, và cũng không thể sửa PATH của bạn — trừ khi bạn source nó. Đây là lý do các công cụ quản lý phiên bản đều bảo bạn thêm một dòng vào file cấu hình shell thay vì chỉ chạy một lệnh cài đặt.

sudo dùng PATH khác của bạn

which congcu             # /usr/local/bin/congcu
sudo congcu              # command not found
sudo env | grep PATH     # PATH hoan toan khac

Vì lý do an toàn, sudo thường được cấu hình thay PATH bằng một danh sách cố định khai báo trong /etc/sudoers. Cách xử lý: gọi bằng đường dẫn tuyệt đối sudo /usr/local/bin/congcu, hoặc nếu thật sự cần thì sửa danh sách đó — bằng visudo, không bao giờ sửa thẳng file.

cron và systemd cũng vậy

Cả hai đều không đọc file cấu hình shell của bạn. Với cron, khai báo PATH= ngay dòng đầu crontab. Với systemd, dùng Environment= hoặc EnvironmentFile= trong phần [Service], và nhớ ExecStart luôn phải là đường dẫn tuyệt đối.

Bảng kiểm khi gặp command not found

  1. File có thật không, tên có đúng không: ls -l /duong/dan/day/du.
  2. File có quyền thực thi không: chmod +x nếu thiếu.
  3. Thư mục chứa nó có trong PATH không: printenv PATH | tr ':' '\n'.
  4. Shell có nhớ nhầm vị trí cũ không: hash -r rồi thử lại.
  5. Đang chạy trong ngữ cảnh nào — terminal, script, cron, sudo hay systemd? Mỗi ngữ cảnh một môi trường riêng.
  6. Nếu là script, dòng đầu tiên có trỏ đúng trình thông dịch không: #!/usr/bin/env bash.

Lần tới gặp command not found, đừng vội cài lại gói. Chạy type -aprintenv PATH trước — hai lệnh này giải thích được phần lớn các trường hợp, và giải thích luôn vì sao cùng một lệnh lại chạy ở chỗ này mà hỏng ở chỗ kia.

Lên đầu trang