命令行的历史和流派:

  • UNIX家族
  • POSIX标准
  • macOS
  • Linux
  • Windows Subsystem for Linux
  • Windows

一、命令的四大要素

命令的组成四要素缺一不可,以下四个要素相同就可以完全地“重现”⼀个命令,你碰到的各种各样古怪的问题,原因⼀定是四个要素之⼀。

  • 可执行程序(Executable)
  • 参数
  • 环境变量(Environment variable)
  • 工作目录(Working directory)

1. 工作目录

启动命令的当前光标所在的路径,相对路径都是相对于这个路径。输入pwd 命令可以查看当前所处的工作目录。
可以这样理解:命令(可执行程序)本身是存在于某个目录的,执行一个命令时需要先找到这个命令,通常根据PATH环境变量来查找可执行程序,或者直接使用该命令的绝对路径(使用which查看),现在拿到这个工具后,不要再关心工具从哪来,要关注干活的地方在哪,而标题中【工作目录】就是这个工具当前干活的地方。

2. 环境变量

变量又分为局部变量和全局的环境变量,环境变量是和环境强绑定的,是一种应用广泛的传递配置的方式,可以使用环境变量向不同程序传递参数和配置,例如CLASSPATHGOPATH
查看所有的环境变量使用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 文件权限

Linux 基础——权限管理命令chmod

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");
    }
}

参考:

  1. linux 中的局部变量、全局变量、shell 变量的总结
  2. Linux 基础——权限管理命令chmod
  3. 《Linux 命令行大全》