命令行的歷史和流派:
- UNIX家族
- POSIX標准
- macOS
- Linux
- Windows Subsystem for Linux
- Windows
一、命令的四大要素
命令的組成四要素缺一不可,以下四個要素相同就可以完全地“重現”⼀個命令,你碰到的各種各樣古怪的問題,原因⼀定是四個要素之⼀。
- 可執行程序(Executable)
- 參數
- 環境變量(Environment variable)
- 工作目錄(Working directory)
1. 工作目錄
啟動命令的當前光標所在的路徑,相對路徑都是相對於這個路徑。輸入pwd
命令可以查看當前所處的工作目錄。
可以這樣理解:命令(可執行程序)本身是存在於某個目錄的,執行一個命令時需要先找到這個命令,通常根據PATH
環境變量來查找可執行程序,或者直接使用該命令的絕對路徑(使用which
查看),現在拿到這個工具后,不要再關心工具從哪來,要關注干活的地方在哪,而標題中【工作目錄】就是這個工具當前干活的地方。
2. 環境變量
變量又分為局部變量和全局的環境變量,環境變量是和環境強綁定的,是一種應用廣泛的傳遞配置的方式,可以使用環境變量向不同程序傳遞參數和配置,例如CLASSPATH
和GOPATH
。
查看所有的環境變量使用export
。
局部變量
局部變量的作用域被限定在創建它們的 shell 中。意思是子進程中不會去繼承。local 函數可以用來創建局部變量,但僅限於函數內使用。局部變量可以通過簡單的賦予它一個值或一個變量名來設置,用 declare 內置函數來設置,或者省略也可。
name=yue
echo $name
環境變量(全局變量)
環境變量又稱全局變量,以區別於局部變量,通常,環境變量應該大寫,環境變量是已經用export
內置命令導出的變量。
臨時的環境變量使用export
直接在命令行中聲明即可,變量在關閉shell時失效:
export NAME=yue
echo $NAME # yue
永久的(對當前用戶永久有效)是需要把export
命令寫在啟動配置文件 ~/.bash_profile
中,語法同上。保存文件后如果希望在當前 shell 中立即生效,執行 source .bash_profile
,否則新打開的 shell 才會生效。
無論是臨時的還是永久的環境變量,子 shell 都會繼承當前父 shell 的環境變量,但不能逆向傳遞。可以去執行bash
來創建一個子 shell 做個試驗。
還可以快速傳遞一個環境變量(只對當前執行的這行命令有效):
NAME=Tony go run main.go
#之后這個環境變量就不存在了
echo $NAME # 空空如也
系統變量
如果你在 Windows 上安裝過 Java 的開發環境,一定還記得配置 PATH 系統環境變量,這樣才能需要時根據這個 PATH 中提到的路徑,找到相應的可執行程序並運行。
所以如PATH
這種系統級的環境變量,比 git bash ~/.bash_profile
里 bash 終端級環境變量的作用域更廣,畢竟操作系統才是爸爸。
想證明一下很簡單,先去設置系統環境變量,比如名叫JUST_TEST
,然后win + R 開一個 cmd,執行 echo%JUST_TEST%
,就可以看到剛才設置的變量值。
進程(Process)
是計算機程序運行的最小單位,獨占自己的內存空間和文件資源,每個進程都和一組環境變量相綁定。子進程是由父進程 fork 出來的,環境變量(全局變量)可以被子進程繼承,所有的操作系統和編程語言都支持環境變量。
例如為當前 shell 設置了環境變量XXX
,然后在當前環境下進入 node 執行環境后,可以通過process.env.XXX
看到環境變量被繼承了(正如上文提到的,局部變量不會被子進程繼承)。
3. 可執行程序
什么算是可執行程序
Windows 中 exe/bat/com
文件擴展名被認為是可執行程序,通過 Path;
UNIX/Linux 中看x
權限(ls -l
),即可執行權限;
去哪⾥找程序?
在 Windows 中是Path 環境變量和當前目錄;
在 UNIX/Linux 中 PATH 環境變量。
可執行程序都是從 path 中尋找路徑,如果設為空字符串,會找不到。
如果當前就在可執行程序的目錄下,對於 UNIX 體系的可以通過 ./xxx
執行,.
代表當前目錄。
而對於 Windows 的 cmd 是直接輸入可執行程序的名稱,至於后綴,加不加都行,會自動尋找exe/bat/com
這樣的后綴。
在腳本的第⼀⾏指定解釋器(shebang)
編輯一個xxx.sh
文件時,可以在 shell 腳本中第一行指定別的解釋器:
#!/usr/bin/env node
console.log(123)
表示在當前執行上下文環境中,查找 node 可執行程序來解釋當前腳本,那么當然會從 path 環境變量中查找 node 的路徑啦,這樣寫其實就等價於直接在命令行中執行 node xxx.sh
。
別名(alias)
~/.bash_profile 是交互式、login 方式進入 bash 運行的
~/.bashrc 是交互式 non-login 方式進入 bash 運行的
.bash_profile 在用戶每次登錄系統時被讀取,里面的所有命令都會被bash執行。
.bashrc文件會在bash shell調用另一個bash shell時讀取,也就是在shell中再鍵入bash命令啟動一個新shell時就會去讀該文件。這樣可有效分離登錄和子shell所需的環境。
一般來說都會在.bash_profile里調用.bashrc腳本以便統一配置用戶環境。
在一個 shell 中使用alias
命令設置的別名,屬於局部變量,只對當前這一層 shell 環境有效,寫在~/.bash_profile
中后,每次新登錄的 shell 都會讀取,但由於alias
配置的別名屬於局部變量,加上創建子 shell 時不會讀取.bash_profile
(除非寫在.bashrc
中),所以也就不會為子 shell 設置別名:
vim ~/.bash_profile
# 寫入如下內容,保存后 source 一下立即生效
export NAME=Tony
export AGE=25
echo '你好哇~'
alias ~='cd ~'
alias cdproject='cd ~/Projects'
每當打開一個登錄終端時,都會看到你好哇~
,這說明每打開一個終端,就相當於系統新 fork 了一個 bash 終端進程,繼承了系統環境變量后,還要執行啟動文件,也就是.bash_profile
。
Linux 文件權限
4. 參數
可執行程序后面所有的都是參數。UNIX 系統約定如下(Java 並沒有嚴格遵守):
約定一:-
后面只能跟一個字符,但可以合並,ls -alth
等價於ls -a -l -t -h
約定二:--
后面跟一個單詞,ls --all
等價於ls -a
參數如果有空格,會以空格分割為多個傳遞給可執行程序;
參數不加引號或" "
雙引號,命令行會對參數進行變量的替換和展開;
而使用' '
單引號,命令行不會做任何特殊處理,這可用來聲明參數是一個整體:
export A=123
echo wan$A.m # wan123.m
echo "wan$A.m" # wan123.m
echo 'wan$A.m' # wan$A.m
如果參數中就是要包含單引號' '
,那么可以再用雙引號" "
包起來或者進行轉義:
echo \'I am a boy\' # 'I am a boy'
echo "'I am a boy'" # 'I am a boy'
二、使用命令編譯運行Java程序
Java 世界里的一切工具都只做一件事:拼接命令行
1. 編譯運行
javac Main.java # 源文件編譯成字節碼
ls # 查看編譯結果 Main.class Mian.java
java Main # 運行
Java 中:
System.getenv()
查看環境變量
System.getProperty()
查看系統屬性
傳遞系統屬性要以D
開頭,要注意書寫位置,如果在Mian
后面就成了Main
的參數了,也就是第一天學 Java 就接觸到的mian
方法中的String[] args
參數。如傳一個名為AAA
,值為123
的屬性:
java -DAAA=123 Main
user.dir
查看當前工作目錄
java.version
查看當前 jdk 版本
2. -classpath(-cp) 參數
import junit.extensions.ActiveTestSuite;
public class Main {
public static void main(String[] args) {
System.out.println(ActiveTestSuite.class.getName());
}
}
直接執行javac Main.java
會報錯找不到。
因此對於引入的第三方類庫,編譯時要用-classpath
來指定 jar 包的查找路徑(假設這個 jar 包就在當前工作目錄下):
javac -cp junit-3.8.2.jar Main.java
這次成功編譯了,因為 jar 包就是個普通的 zip 文件,里面放了一堆符合類文件。一個類的全限定類名(FQCN)的包名是和文件夾一一對應的。
這個命令里,javac
是 executable 可執行程序,后面全都是參數,-classpath(-cp)
指定了 jar 包路徑,Main.java
是即將被編譯的文件。Main.java
中有一個ActiveTestSuite
,這個類肯定不能從天上掉下來,要去哪兒找呢,就只能去-cp
指定的地方找。
接下使用java
命令來執行有個天坑,在 UNIX 環境中和 Windows 環境中是有區別的,先說在 UNIX 環境下:
java -cp junit-3.8.2.jar:. Main
以冒號:
分隔路徑,.
代表同時也在當前目錄下查找,第二個java
命令Main
代表告訴 JVM 要從Main
類啟動程序,那么Main
類從哪兒找呢?只能從-cp
指定的路徑找(即.
所代表的當前目錄),JVM 運行Main
的時候發現引用了ActiveTestSuite
類,繼續從-cp
指定的路徑中查找。
以上命令在 Windows 中的 git bash 里執行時有個天坑,執行會報錯。雖然看似在 git bash 中執行了命令,但是-cp
后面的路徑還是要交給 Windows 版本的java
可執行程序去解析的,而在 Windows 版本 classpath 的路徑分隔符是用分號;
而不是冒號:
,但如果只是簡單的冒號換成分號還是不行,因為 UNIX 環境中又會用分號來分割命令(bash 中執行一下mkdir testDir; cd testDir
試試就知道了),所以要再加單引號' '
,表示不對路徑參數做任何參數解析,原樣交給Java
命令。
java -cp 'junit-3.8.2.jar;.' Main
三、Java中fork子進程
java-fork-process/working-directory/run.sh:
#!/usr/bin/env sh
echo "AAA is: $AAA"
ls -alth
java-fork-process/Fork.java:
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
public class Fork {
public static void main(String[] args) throws Exception {
// 使用Java代碼fork一個子進程,將fork的子進程的標准輸出重定向到指定文件:工作目錄下名為output.txt的文件
// 工作目錄是項目目錄下的working-directory目錄(可以用getWorkingDir()方法得到這個目錄對應的File對象)
// 傳遞的命令是sh run.sh 假設working-directory目錄下存在 run.sh 腳本文件
// 環境變量是AAA=123
// 1.可執行程序 2.參數
ProcessBuilder pb = new ProcessBuilder("sh", "run.sh");
// 3.工作目錄
pb.directory(getWorkingDir());
// 4.環境變量
Map<String, String> env = pb.environment();
env.put("AAA", "123");
env.get("AAA");
pb.redirectOutput(getOutputFile());
pb.start().waitFor();
}
private static File getWorkingDir() {
Path projectDir = Paths.get(System.getProperty("user.dir"));
return projectDir.resolve("working-directory").toFile();
}
private static File getOutputFile() {
return new File(getWorkingDir(), "output.txt");
}
}
參考: