一:命令行入门

Kernel 与 Shell

Kernel

Kernel 是操作系统的内核,是操作系统的核心部分。它由操作系统中用于管理存储器,文件,外设和系统资源的那些重要部分组成。操作系统内核通常运行进程,并提供进程间的通信。

内核是大多数操作系统的核心部分,但是出于安全性考虑,用户无法直接与这一部分进行交互。

Shell

在计算机科学中,Shell 俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它类似于 DOS 下的 command.com 和后来的 cmd.exe。它用于接收用户命令,然后调用相应的应用程序。

Shell 可以理解为在内核之外的一层壳,是用户与内核进行交互的接口。

因为用户无法与内核直接进行交互,但是与内核的交互又是必不可缺的,于是 Shell 便充当了用户与核之间的桥梁。

KernelShell 关系示意图如下:

image.png

命令行

广义的命令行包括一切通过字符终端控制计算机的方式:

  • Windows

    • cmd
    • PowerShell
    • Git bash
    • … …
  • UNIX/Linux


  • sh
  • zsh
  • Terminal
  • … …

二:工作目录和环境变量

命令行的全部要素

  • 可执行程序Executable

  • 参数

  • 环境变量Environment variable

  • 工作目录Working directory

工作目录

  • 启动命令的路径

  • 相对路径/绝对路径

环境变量

  • 进程(Process

    • 进程是计算机程序运行的最小单位

    • 独占自己的内存空间和文件资源

  • 每个进程都和一组变量相互绑定

    • 传递不同的环境变量可以让程序表现出不同的行为

    • CLASSPATH/GOPATH

  • 在进程的 fork 过程中,环境变量可以被完全继承

  • 所有的操作系统/编程语言都支持环境变量

  • 局部和全局的环境变量

环境变量实战

通过 export/set 设置环境变量,通过 echo 读取环境变量

首先,我们可以使用 export 来获取所有的环境变量

我们还可以通过 export 命令设置环境变量,环境变量本身就是一个键值对

  1. export AAA = 12345

使用 echo 命令,读取设置的环境变量;我们需要在获取的环境变量 key 前加上 ‘$’ 符号

  1. ~ echo $AAA
  2. 12345

使用 Java/Go 读取环境变量

在我的根目录下有 Main.java 文件和 main.go 文件

Main.java

  1. public class Main {
  2. public static void main(String[] args) {
  3. System.out.println(System.getenv("AAA"));
  4. }
  5. }

执行命令:

  1. javac Main.java && java Main

返回结果:

  1. ~ javac Main.java && java Main
  2. 12345

Go 语言也支持获取环境变量

main.go

package main

import "fmt"
import "os"

func main(){
    fmt.Println(os.Getenv("AAA"))
}

执行命令:

go run main.go

返回结果:

➜  ~ go run main.go
12345

不只是 Java 还有 Go,几乎所有的语言都支持获取当前的环境变量。

环境变量的作用域

在我的当前的 Terminal 窗口,我设置了一个环境变量 AAA = 12345

image.png

可以看到,我们是能够在当前的窗口中获取到这个环境变量的。

我们新开一个窗口,再次获取下这个变量:

image.png

在这个新开的命令行窗口中,我们无法获取到 ‘AAA’ 这个环境变量了,因为 ‘AAA’ 这个环境变量只对当前的窗口这个作用域有效,所以在第二个命令行窗口是无法获取到这个局部的环境变量。

我们使用命令:

AAA=1 go run main.go

返回结果为:

➜  ~ AAA=1 go run main.go
1

环境变量 AAA 的值有被改变么?

➜  ~ echo $AAA
12345

答案是并没有,我们之所以运行 main.go 程序返回的结果为 1 是因为,‘AAA=1’ 这个环境变量只对当前运行的 Go 程序有效。

如何让环境变量永久生效且变为全局有效呢?

我们只需要将环境变量添加到 ~/.bash_profile 当中即可。

因为我的终端配置了 oh-my-zsh ,所以我需要在 ~/.zshrc 中配置永久环境变量。

添加:

export AAA=12345

配置完毕后,我们需要执行命令:

source ~/.zshrc

这样才会让配置项立即生效。

现在 ‘AAA=12345’ 就是全局可用且永久的环境变量了

向 Docker 容器传递环境变量

示例:

使用 Docker 开启一个 ubuntu 容器,并传递环境变量 ‘AAA=123’

docker run -it -e AAA=123 ubuntu

进入到容器内部,我们可以直接获取到该环境变量:

➜  ~ docker run -it -e AAA=123 ubuntu
root@038852dd3784:/# echo $AAA
123

三:可执行程序

在我们输入一个命令时:

java -version

这个命令里面,第一个参数 java 就是一个可执行程序

在不同的操作系统中,可执行程序的定义是不一样的,Windows 系统中,有 exe/bat/com 后缀名的文件就是可执行程序;而在 UNIX/Linux 系统中,拥有 x 权限的文件就是一个可执行程序。

  • Windows:exe/bat/com

  • UNIX/Linux:x 权限

Windows 可执行文件名的后缀名大家都可以非常容易地理解,关于 UNIX/Linuxx 权限我们怎么去理解呢?

UNIX/Linux 中,每个文件或目录都有 9 个基础权限位,每三个字符被分成一组,分别代表属主权限位,用户组权限位,其他用户权限位

r 代表可以读,w 代表可写,x 代表可执行。

我们可以通过 ls -al 命令来查看一个文件的权限。

我们在家目录下,有一个写好的 main.go 程序,使用命令 go build main.go 会将该程序编译生成可执行文件 main ,我们来看查看下该文件的权限信息:

~ ls -al main
-rwxr-xr-x  1 macbook  staff  2142728  5 17 20:33 main

我们现在输入该命令:

➜  ~ ./main
12345

如我们所想,该可执行程序返回了 ‘12345’

如何将该程序变为不可执行的?

chmod 666 命令会去掉一个可执行程序的可执行权限

实际上,我们就是将这个文件的权限变为 rw-rw-rw-

chmod 666 ./main

现在,我们再来查看 main 的权限信息:

➜  ~ ls -al main
-rw-rw-rw-  1 macbook  staff  2142728  5 17 20:33 main

现在 main 已经不具备可执行条件了:

➜  ~ ./main
zsh: permission denied: ./main

我们可以使用 chmod 777 命令来重新赋予该程序可执行权限

实际上,我们就是将这个文件的权限变为 rwxrwxrwx

➜  ~ chmod 777 ./main

查看 main 的权限信息:

➜  ~ ls -al main
-rwxrwxrwx  1 macbook  staff  2142728  5 17 20:33 main

执行 main

➜  ~ ./main
12345

那么我们在命令行中输入可执行程序,命令行是从哪里找程序的呢?

Windows 系统中,命令行会在当前目录和 Path 环境变量中寻找,在 UNIX/Linux 系统中,只会从 PATH 环境变量中寻找。并且注意的是,UNIX/LinuxPATH 四个字母均为大写

  • Windows:Path 环境变量 + 当前目录
  • UNIX/Linux:只会在 PATH 环境变量中查找

Shebang

Shebang 的名字来源于 SHArpbang,指代两个符号 #!

Shebang 通常出现在 Unix 系统脚本中的第一行,作为前两个字符,用于指明执行这个脚本文件的解析器。

我在我的家目录下新建一个 node 脚本 console.sh,内容为:

console.log(123)

执行该文件:

➜  ~ ./console.sh
zsh: permission denied: ./console.sh

我们看到该脚本无法执行,无法执行的原因就是,虽然我们知道这是一行 node 代码,但是命令行并不知道究竟该让哪个可执行文件去执行该程序

我们可以指定 node 来执行该可执行脚本:

➜  ~ /usr/bin/env node ./console.sh
123

也可以在文件头指定该文件的程序解析器,修改 console.sh:

#! /usr/bin/env node
console.log(123)

然后直接执行 console.sh :

➜  ~ ./console.sh
123

我们发现,该脚本可以直接执行了。

alias

alias 可以给你的命令取个别名

例如:

alias gst='git status'

修改后,当我输入 gst 时,它就等效于 git status

alias 命令的好处就是简化你的命令输入,让你输入一些比较长的命令时可以更加快速便捷

如果你希望别名能够一直有效,还是需要在你的环境变量配置文件中进行修改。

四:参数

我们输入命令:

ls -alth

ls 是可执行文件,后面的 -alth 是参数

参数当中的单引号和双引号引发的惨案

小明写了一个 Java 程序:

Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println(System.getenv("AAA"));
    }
    class A {
    }
}

通过 javac 命令编译源文件后,在目录下出现了两个编译后的 class 文件

➜  ~ ls -al *.class
-rw-r--r--  1 macbook  staff  301  5 17 21:50 Main$A.class
-rw-r--r--  1 macbook  staff  539  5 17 21:50 Main.class

一个是 Main$A.class,另一个是 Main.class

小明想使用 javap 来查看 Main$A.class 文件,他输入的命令是这样的:

➜  ~ javap Main$A.class
Compiled from "Main.java"
public class Main {
  public Main();
  public static void main(java.lang.String[]);
}

但是,小明看到旁边的小红得到了和自己完全不一样的反编译结果,小红得到的反编译结果是这样的:

Compiled from "Main.java"
class Main$A {
  final Main this$0;
  Main$A(Main);
}

小明知道小红每次都是班级第一,她的答案肯定不会错 ,于是他虚心地向小红求教。小红也是一个乐善好施的孩子,她看了下小明输入的命令,发现了端倪。

小红说:“你的命令行并没有添加单引号,在你输入 javap Main$A.class 时,命令行会将 $ 符当作将变量的展开,因为命令行没有发现 A 对应的值是什么,所以它会将其看作空字符串进行处理。所以,小明,你输入的命令等价于 javap Main.class !”

小明不信邪,小红说:“不妨你将 A 的值设置成一个环境变量,比如:export A=123,这个时候,如果你还是运行 javap Main$A.class 一定会报错。”

小明试了下,果不其然!

➜  ~ export A=123
➜  ~ javap Main$A.class
错误: 找不到类: Main123.class

小明现在对小红真的是五体投地,不过他心里还是有一个疑问,小明好奇地问道:“不过我还是有一点不解,为什么非要用单引号呢?如果我使用双引号不可以吗?”

小红微微一笑,说道:“实践出真知!你为什么不自己试一下呢?”

小明脸一红,赶忙自己敲下了命令:

➜  ~ javap "Main$A.class"

错误: 找不到类: Main123.class

“看来,双引号也会将 $ 符作为变量的展开!我堂堂小明差点被单引号和双引号骗傻了”,小明感叹道。

小红听了小明的话忍俊不禁… …

五:输入与输出

  • 标准输入 stdin

  • 标准输出 stdout

  • 标准错误 stderr

  • 输出的重定向

    • 覆盖文件
    • 追加文件
    • 改变流向
    • /dev/null

在 Java 语言中的标准输出与标准错误
public class Main {
    public static void main(String[] args) {
        // 标注输出
        System.out.println("standard output");
        // 标准错误
        System.err.println("standard error");
    }
}

输出的重定向:覆盖文件

如果我们执行命令:

javac Main.java && java Main

我们的命令行窗口会输出内容:

➜  ~ javac Main.java && java Main
standard output
standard error

如果我有一个特殊的需求,需要将输出的内容打印到文件中,我们可以执行这样的命令:

~ java Main > out.txt
standard error

或:

➜  ~ java Main 1> out.txt
standard error

>1> 都代表将标准输出以覆盖的形式重定向到文件中。

这个时候,我们看到命令行中只显示打印了 standard error,查看 out.txt 的内容:

➜  ~ cat out.txt
standard output

我们看到 standard output 成功打印到了 out.txt 文件中

如果我们想要将标准错误的内容打印到文件中,我们可以执行这样的命令:

➜  ~ java Main 2> out.txt
standard output

2> 代表将标准错误以覆盖的形式重定向到文件中。

这个时候,我们看到命令行中只显示打印了 standard output,查看 out.txt 的内容:

➜  ~ cat out.txt
standard error

我们看到 standard error 成功打印到了 out.txt 文件中

输出重定向:追加文件

我们上面的演示用例都只是覆盖文件,如果我们想要在文件的基础上追加内容也非常简单,只需要将覆盖文件符号> 变为 >>

➜  ~ cat out.txt
standard error

目前,我的 out.txt 文件中有一行内容:standard error

我想在这个文件的基础上,将我的输出错误内容追加,我们只需执行命令:

➜  ~ java Main 2>> out.txt
standard output

现在,我们再来查看 out.txt 文件:

➜  ~ cat out.txt
standard error
standard error

可以看到我们成功实现了输出重定向追加内容的功能

输出重定向:改变流向

现在,我们希望我们可以将标准输出和标准错误的打印内容一起重定向到文件中

我们的 out.txt 文件现在是这样的:

➜  ~ cat out.txt
standard error
standard error

我们现在将标准输出和标准错误的打印内容一起追加到 out.txt 中,执行命令:

java Main >> out.txt 2>&1

该命令的含义是,首先将标准错误重定向到标准输出,然后将标准输出重定向到 out.txt

我们现在来看下 out.txt 文件的内容:

➜  ~ cat out.txt
standard error
standard error
standard output
standard error

成功~

输出重定向:/dev/null

/dev/null 是一个垃圾桶的概念,比如我们希望只打印标准错误,将标准输出重定向到“垃圾桶”:

➜  ~ java Main >> /dev/null
standard error

六:Linux 常用命令详解

  1. cd

change the working directory

改变目录,例如:cd ~/Desktop

  1. pwd

print name of current / working directory

显示当前目录位置

  1. mkdir

make directories

参数:-p ;含义:no error if existing,make parent directories as needed

创建目录

示例一:mkdir test1 test2 test3 , 该命令可以在当前目录下创建三个同级目录

示例二:mkdir -p "test1/test2/test3" , 该命令可以创建子目录

  1. ls

list directory contents

参数:-a ; 含义:do not ignore entries starting with .
参数:-l ;含义:use a long listing format
参数:-t;含义:sort by modification time,newest first
参数:-h ;含义:print sizes in human readable format

  1. echo

display a line of text

示例:echo "I love you" > test.txt ; 将 “I love you” 重定向到 test.txt 文件中

  1. touch

change file timetamps

示例:touch test.txt;如果该文件不存在,则创建 test.txt,如果该文件已经存在,则改变该文件最后一次的修改时间

  1. cp

copy files and directories

参数:-r ; 含义:copy directories recursively

用法:cp 源路径 目标路径;cp -r 源路径 目标路径

示例:

    mkdir test1 test2
    cd test1
    echo "1" > test1.txt
    cd ../
    cp test1/test1.txt test2/test2.txt

执行上述命令后,会看到在 test2 目录下的 test2.txt 文件,并且,test2.txt 文件的内容为 “1”

  1. mv

move(rename) files

示例:

        mkdir test1 test2
    cd test1
    echo "1" > test1.txt
    cd ../
    mv test1/test1.txt test2/test2.txt

执行上述命令后,相当于将 test1 目录下的 test1.txt 剪切到 test2 目录中,并重命名为 test2.txt

  1. rm

remove files or directories

参数:-r ; 含义:remove directories recursively
参数:-f ; 含义:force,强制删除

示例:

rm -rf test

该命令会强制删除 test 目录及目录下的所有文件

  1. cat

concatenate files and print on the standard output

简言之:查看文件内容

七:使用命令编译运行 Java 程序

java 和 javac

javac 编译器是官方 JDK 提供的前端编译器,我们知道,java 源文件并不会被 JVM 解释执行,要先由编译器将源文件编译为 .class 字节码文件;

java 命令的作用就是执行字节码文件,这里需要注意的是:我们需要使用全限定类名。

Program arguments

我们可以给程序传入一些参数,如示例:

Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println("Args size: " + args.length);
        System.out.println("First argument is: " + args[0]);
        System.out.println("Second argument is: " + args[1]);
        System.out.println("Third argument is: " + args[2]);
    }
}

编译源文件:

javac Main.java

运行 Main.class;我们在该命令后添加了三个参数,程序的运行结果如下:

➜  ~ java Main 1 2 3
Args size: 3
First argument is: 1
Second argument is: 2
Third argument is: 3

System property

JVM 内部,有一组和环境变量非常相似的东西叫做系统属性

我们可以使用 -D 参数来设定系统属性,例如 -DAAA=123

java 语言中,我们则可以使用 System.getProperty(); 来获取系统属性

并在在 JVM 内部,有一些预先设定好的系统属性,例如:

  • java.version : Java 版本号
  • user.dir:工作目录
  • … …

示例程序:

Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println("system property : " + System.getProperty("AAA"));
        System.out.println("system property : " + System.getProperty("java.version"));
        System.out.println("system.property : " + System.getProperty("user.dir"));
    }
}

编译源文件:

javac Main.java

运行 Main.class;我们在该命令后设置了系统属性 AAA=123,程序的运行结果如下:

➜  ~ java -DAAA=123 Main
system property : 123
system property : 11.0.8
system.property : /Users/macbook

classpath

classpath 其实不难理解,就是 class + path ,即:类文件的路径

我们可以使用 -classpath-cp 参数指定配置/资源文件的路径

示例:

Main.java

import org.apache.commons.lang3.StringUtils;

public class Main {
    public static void main(String[] args) {
        System.out.println(StringUtils.isBlank(""));
    }
}

编译源文件时,我们发现会报错:

➜  ~ javac Main.java
Main.java:1: 错误: 程序包org.apache.commons.lang3不存在
import org.apache.commons.lang3.StringUtils;
                               ^
Main.java:5: 错误: 找不到符号
        System.out.println(StringUtils.isBlank(""));
                           ^
  符号:   变量 StringUtils
  位置: 类 Main
2 个错误

编译报错的原因就是我们找不到 org.apache.commons.lang3.StringUtils

我将 commons-lang3-3.9.jar 这个 JAR 包放在与 Main.java 同级目录下,并且加上 classpath 进行指定:

javac -classpath commons-lang3-3.9.jar Main.java

发现编译不再报错,可以通过~

运行 Main.class ;同理,我们需要指定 StringUtilsclasspath ,还需要指定 Main.classclasspath ,因为 Main.class 就在当前目录中,所以直接使用 “.” 来表示两个变量之间使用 “:” 分割

➜  ~ java -cp commons-lang3-3.9.jar:. Main
true

八:Java 中 fork 子进程

如何使用 Java 程序去 fork 子进程

示例:

run.sh

#!/usr/bin/env sh
echo "AAA is: $AAA"
ls -alth

Fork

package com.github.hcsp.shell;

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
        // 环境变量是AAA=123
        ProcessBuilder pb = new ProcessBuilder("sh", "run.sh");
        pb.redirectOutput(getOutputFile());
        pb.directory(getWorkingDir());
        Map<String, String> environment = pb.environment();
        environment.put("AAA", "123");
        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");
    }
}