开源技术 * IBM 微讲堂:Kubeflow 系列(观看回放 | 下载讲义) 了解详情

自定义和使用 shell 环境

概述

在本教程中,学习自定义 Linux bash shell 环境来满足用户需求。学习:

  • 修改全局和用户配置文件
  • 在登录或生成新 shell 时设置环境变量
  • 为常用命令序列创建 bash 函数
  • 为新用户帐户维护框架目录
  • 设置命令搜索路径

Linux shell

您在终端上使用一个 Linux shell 程序,通过键入命令( 输入流 )来与系统交互,并在同一个终端上查看输出( 输出流 )和错误消息( 错误流 )。有时,您需要在系统引导以前运行命令,以便允许终端建立连接,有时您需要定期运行命令,无论您是否登录。shell 也可以为您完成这些任务。标准输入和输出流不需要来自或定向到终端上的真实用户。在本教程中,将进一步了解 shell 和自定义用户的环境。具体地讲,您将学习 bash (Bourne again) shell(原始 original Bourne shell 的增强版),并了解使 bash 更符合可移植操作系统接口 (Portable Operating System Interface, POSIX) 标准的更改。我会在此过程中介绍其他一些 shell 特性。

本教程帮助针对 Linux Server Professional (LPIC-1) 考试 102 的主题 105 中的目标 105.1 进行应考准备。该目标的权重为 4。

前提条件

要从本系列教程中获得最大收获,您应该拥有 Linux 的基本知识和一个正常工作的 Linux 系统,您可以在这个系统上实践本教程中涵盖的命令。您应该熟悉 GNU 和 UNIX 命令。本教程以针对考试 101 的主题 103 的教程中介绍的材料为基础。有时一个程序的不同版本会以不同方式格式化输出,所以您的结果可能并不总是与这里所示的清单和图像相同。

本教程中的示例很大程度上与发行版无关。除非另行说明,这些示例使用的是 Fedora 22 和 4.2.3 内核。

Shell 和环境

shell 在您与操作系统的复杂细节之间提供了一个层。借助 Linux(和 UNIX)shell,您可以通过组合基本函数来构建复杂的操作。然后可以使用编程结构,构建能在 shell 中直接执行的函数或将函数保存为 shell 脚本 。shell 脚本是一个命令序列,它存储在 shell 可根据需要来运行的文件中。

POSIX 是一系列 IEEE 标准,它们统称为 IEEE 1003。(第一个 POSIX 标准是 1988 年发布的 IEEE Standard 1003.1-1988。)Bash 实现了许多 POSIX 特性,可在一种更符合 POSIX 标准的模式下运行。其他著名的 shell 包括 Korn shell (ksh)、C shell (csh) 和它的衍生产品 tcsh,以及 Almquist shell (ash) 及其 Debian 衍生产品 (dash)。建议您至少非常熟悉其他这些 shell,这样您才会认出一个脚本何时使用了来自任何这些 shell 的特性。

回想一下教程 “学习 Linux,101:Linux 命令行 ”,当您在 bash shell 中运行时,您有一个 shell 环境 。该环境是一组名称 – 值对,定义了您的提示符的形式、主目录、工作目录、shell 的名称、您打开的文件、您定义的函数,等等。该环境可供每个 shell 进程使用。当一个 shell 启动时,它会将来自环境的值分配给 shell 变量 。您可以使用 shell(包括 bash)创建和修改其他 shell 变量。然后可以导出这些变量作为环境的一部分,供您从当前 shell 生成的子进程继承。

Shell 变量有一个名称(或标识符)。 常见 bash 环境变量 给出了一些通常会自动设置的常见 bash 环境变量。

常见 bash 环境变量
名称 用途
USER 登录用户的名称
UID 登录用户的用户 ID 数字
HOME 用户的主目录
PWD 当前工作目录
SHELL shell 的名称
PPID 父进程的 PID — 启动此进程的进程的进程 ID

除了变量之外,shell 还会设置一些特殊参数,但这些参数无法修改。 常见 bash 参数 给出了一些例子。

常见 bash 参数
名称 用途
$ (运行的 bash shell [ 或其他 ] 进程)的进程 ID(或 PID)
? 上一个命令的退出代码
0 shell 或 shell 脚本的名称

使用变量

可以在名称前面加上 $ 作为前缀来使用变量的值,如 使用变量值 所示。

使用变量值
[ian@atticf22 ~]$ echo $UID
1000
[ian@atticf22 ~]$ echo $HOME
/home/ian

设置变量值并让它们可用

可通过键入一个名称后不留空格,立即键入等号 (=),在 bash shell 中创建或设置 shell 变量。变量名是仅由字母数字字符和下划线组成的单词。名称以字母字符或下划线开头。变量名是区分大小写的,所以 var1VAR1 是不同的变量。变量名(特别是导出变量)通常采用大写形式,就像 常见 bash 环境变量 中的示例一样,但这是一种约定,而不是一种要求。一些变量(比如 $$$? )实际上是 shell 参数而不是变量 — 只能引用它们,不能向它们赋值。

Shell 变量仅对您创建它们时所在的进程可见,除非 导出 它们,以便子进程可以看到和使用它们。子进程不能将变量导出到父进程。可以使用 export 命令导出变量。在 bash shell 中,可以一步完成分配和导出,但不是所有 shell 都支持这项功能。

探索这些概念的一种好方法是使用另一个 shell 来创建子进程。可以使用 ps 命令帮助跟踪您所在的位置和正在运行的进程。 设置 shell 变量并将它们导出到环境 给出了一些示例,并提供了行内注释来帮助您理解。

设置 shell 变量并将它们导出到环境
[ian@atticf22 ~]$ # Use the ps command to list current PID, parent PID and running command name
[ian@atticf22 ~]$ ps -p $$ -o "pid ppid cmd"
 PID  PPID CMD
12761  9169 bash
[ian@atticf22 ~]$ # Start a child bash process
[ian@atticf22 ~]$ bash
[ian@atticf22 ~]$ # Assign two variables
[ian@atticf22 ~]$ VAR1=v1
[ian@atticf22 ~]$ VAR2="Variable 2"
[ian@atticf22 ~]$ # Export examples
[ian@atticf22 ~]$ export VAR2
[ian@atticf22 ~]$ export VAR3="Assigned and exported in one step"
[ian@atticf22 ~]$ # Use the $ character to reference the variables
[ian@atticf22 ~]$ echo $VAR1 '/' $VAR2 '/' $VAR3
v1 / Variable 2 / Assigned and exported in one step
[ian@atticf22 ~]$ # What is the value of the SHELL variable?
[ian@atticf22 ~]$ echo $SHELL
/bin/bash
[ian@atticf22 ~]$ # Now start ksh child and export VAR4
[ian@atticf22 ~]$ ksh
$ ps -p $$ -o "pid ppid cmd"
 PID  PPID CMD
26212 22923 ksh
$ export VAR4=var4
$ # See what is visible
$ echo $VAR1 '/' $VAR2 '/' $VAR3 '/' $VAR4 '/' $SHELL
/ Variable 2 / Assigned and exported in one step / var4 / /bin/bash
$# No VAR1 and shell is /bin/bash - is that right?
$ exit
[ian@atticf22 ~]$ ps -p $$ -o "pid ppid cmd"
 PID  PPID CMD
22923 12761 bash
[ian@atticf22 ~]$ # See what is visible
[ian@atticf22 ~]$ echo $VAR1 '/' $VAR2 '/' $VAR3 '/' $VAR4 '/' $SHELL
v1 / Variable 2 / Assigned and exported in one step / / /bin/bash
[ian@atticf22 ~]$ # No VAR4 - our child cannot export back to us
[ian@atticf22 ~]$ exit
exit
[ian@atticf22 ~]$ ps -p $$ -o "pid ppid cmd"
 PID  PPID CMD
12761  9169 bash
[ian@atticf22 ~]$ # See what is visible
[ian@atticf22 ~]$ echo $VAR1 '/' $VAR2 '/' $VAR3 '/' $VAR4 '/' $SHELL
/ / / / /bin/bash
[ian@atticf22 ~]$ # None of VAR1 through VAR4 is exported back to parent

设置 shell 变量并将它们导出到环境 中您是否注意到,ksh 没有设置 SHELL 变量?在您登录时,或者使用 su 命令和创建 登录 shell 的选项来切换到另一个用户时,通常会设置此变量。本教程后面部分会进一步介绍登录 shell。

可使用 echo 命令查看 常见 bash 环境变量常见 bash 参数 中的一些常见 bash 变量和参数的内容,如 常见环境变量和 shell 变量 所示。

常见环境变量和 shell 变量
 [ian@atticf22 ~]$ echo $USER $UID
 ian 1000
 [ian@atticf22 ~]$ echo $SHELL $HOME $PWD
 /bin/bash /home/ian /home/ian
 [ian@atticf22 ~]$ (exit 0);echo $?;(exit 4);echo $?
 0
 4

 [ian@atticf22 ~]$ echo $0
 bash
 [ian@atticf22 ~]$ echo $$ $PPID
 12761 9169
 [ian@atticf22 ~]$ # see what my process and its parent are running
 [ian@atticf22 ~]$ ps -p $$,$PPID -o "pid ppid cmd"
  PID  PPID CMD
 9169 1 /usr/libexec/gnome-terminal-server
 12761  9169 bash

在 bash shell 中,还可以通过在命令前加上名称 = 值对作为前缀,将环境值设置为在单个命令的持续时间内有效,如 为单个命令设置 bash 环境值 所示。

为单个命令设置 bash 环境值
[ian@atticf22 ~]$ echo "$VAR5 / $VAR6"
/
[ian@atticf22 ~]$ VAR5=5 VAR6="some value" bash
[ian@atticf22 ~]$ echo "$VAR5 / $VAR6"
5 / some value
[ian@atticf22 ~]$ exit
[ian@atticf22 ~]$ echo "$VAR5 / $VAR6"
/

readonly 和其他变量属性

我提到过一些 shell 参数无法修改。还可以将变量限制为 readonlyintegerstring ,当然还有其他可能性。可以使用 declare 命令设置变量属性。可使用 -p 选项显示变量和各种属性。要进一步了解 declare 命令,可尝试运行:

info bash "Shell Builtin Commands" "Bash Builtins"  --index-search declare

help declare

变量属性 给出了一些示例。

变量属性
 [ian@atticf22 ~]$ declare -r rov1="this is readonly"
 [ian@atticf22 ~]$ rov="Who says it's read only?"
 [ian@atticf22 ~]$ readonly rov2="another constant value"
 [ian@atticf22 ~]$ rov2=3
 bash: rov2: readonly variable
 [ian@atticf22 ~]$ UID=99
 bash: UID: readonly variable
 [ian@atticf22 ~]$ declare -pr
 declare -r BASHOPTS="checkwinsize:cmdhist:complete_fullquote:expand_aliases:extglob:extquote:
 force_fignore:histappend:interactive_comments:progcomp:promptvars:sourcepath"
 declare -ir BASHPID
 declare -r BASH_COMPLETION_COMPAT_DIR="/etc/bash_completion.d"
 declare -ar BASH_VERSINFO='([0]="4" [1]="3" [2]="42" [3]="1" [4]="release" [5]=
"x86_64-redhat-linux-gnu")'
 declare -ir EUID="1000"
 declare -ir PPID="12761"
 declare -r SHELLOPTS="braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor"
 declare -ir UID="1000"
 declare -r rov1="this is readonly"
 declare -r rov2="another constant value"
 [ian@atticf22 ~]$ help declare
 declare: declare [-aAfFgilnrtux] [-p] [name[=value] ...]
    Set variable values and attributes.

    Declare variables and give them attributes.  If no NAMEs are given,
    display the attributes and values of all variables.

    Options:
      -f   restrict action or display to function names and definitions
      -F   restrict display to function names only (plus line number and
       source file when debugging)
      -g   create global variables when used in a shell function; otherwise
       ignored
      -p   display the attributes and value of each NAME

    Options which set attributes:
      -a   to make NAMEs indexed arrays (if supported)
      -A   to make NAMEs associative arrays (if supported)
      -i   to make NAMEs have the `integer' attribute
      -l   to convert NAMEs to lower case on assignment
      -n   make NAME a reference to the variable named by its value
      -r   to make NAMEs readonly
      -t   to make NAMEs have the `trace' attribute
      -u   to convert NAMEs to upper case on assignment
      -x   to make NAMEs export

    Using `+' instead of `-' turns off the given attribute.

    Variables with the integer attribute have arithmetic evaluation (see
    the `let' command) performed when the variable is assigned a value.

    When used in a function, `declare' makes NAMEs local, as with the `local'
    command.  The `-g' option suppresses this behavior.

    Exit Status:
    Returns success unless an invalid option is supplied or a variable
    assignment error occurs.

取消设置变量

可以使用 unset 命令从 bash shell 删除一个变量。可使用 -v 选项确保删除了变量定义。函数(本教程后面将会介绍)可拥有与变量相同的名称,所以如果想要删除一个函数定义,可使用 -f 。如果没有 -f-v ,bash unset 命令会在变量定义存在时删除变量定义,在函数定义存在时删除函数定义。 取消设置 bash 变量 给出了一些例子。

取消设置 bash 变量
 [ian@atticf22 ~]$ bash
 [ian@atticf22 ~]$ VAR1=v1
 [ian@atticf22 ~]$ declare -i VAR2
 [ian@atticf22 ~]$ VAR2=3+4
 [ian@atticf22 ~]$ echo $VAR1 $VAR2
 v1 7
 [ian@atticf22 ~]$ unset VAR1
 [ian@atticf22 ~]$ echo $VAR1 $VAR2
 7
 [ian@atticf22 ~]$ unset -v VAR2
 [ian@atticf22 ~]$ echo $VAR1 $VAR2

 [ian@atticf22 ~]$ exit
 exit

请注意,如果某个变量被定义为 integer ,向它赋的值会计算为算术表达式。

默认情况下,bash 将未设置的变量视为拥有空值。那么为什么取消设置一个变量,而不是向它分配一个空值?在 bash 和其他许多 shell 中,如果应用未定义的变量,可能会产生一个错误。可使用 set -u 命令生成未定义变量的错误,使用 set +u 禁用该警告。 错误和取消设置变量 演示了这些要点。

错误和取消设置变量
 [ian@atticf22 ~]$ bash
 [ian@atticf22 ~]$ set -u
 [ian@atticf22 ~]$ VAR1=var1
 [ian@atticf22 ~]$ echo $VAR1
 var1
 [ian@atticf22 ~]$ unset VAR1
 [ian@atticf22 ~]$ echo $VAR1
 bash: VAR1: unbound variable
 [ian@atticf22 ~]$ VAR1=
 [ian@atticf22 ~]$ echo $VAR1

 [ian@atticf22 ~]$ unset VAR1
 [ian@atticf22 ~]$ echo $VAR1
 bash: VAR1: unbound variable
 [ian@atticf22 ~]$ unset -v VAR1
 [ian@atticf22 ~]$ set +u
 [ian@atticf22 ~]$ echo $VAR1

 [ian@atticf22 ~]$ exit
 exit

您已看到,取消设置一个不存在的变量不会出错,甚至在指定了 set -u 时也是如此。

环境、变量和 C shell

csh 和 tcsh shell 对环境和变量的处理与 bash 稍微不同。可以使用 set 在 shell 中设置变量,使用 setenv 命令设置和导出变量。语法与 bash export 命令稍有不同,如 在 C 和 tcsh shell 中设置变量和环境值 所示。请注意使用 set 时的等号 (=)。

在 C 和 tcsh shell 中设置变量和环境值
[ian@atticf22 ~]$ tcsh
[ian@atticf22 ~]$ setenv E1 "Env variable 1"
[ian@atticf22 ~]$ set V2="variable 2"
[ian@atticf22 ~]$ echo "$E1 / $V2"
Env variable 1 / variable 2
[ian@atticf22 ~]$ tcsh
[ian@atticf22 ~]$ echo $E1
Env variable 1
[ian@atticf22 ~]$ echo $V2
V2: Undefined variable.
[ian@atticf22 ~]$ echo "$?E1 / $?V2"
1 / 0
[ian@atticf22 ~]$ exit

请注意,第二个 tcsh shell 没有继承 V2 变量。尝试引用 V2 会生成错误,这与在 bash 中设置 set -u 时一样。在 csh 和 tcsh 中,可使用 $?NAME 结构检查是否设置了 NAME ,如果已设置 NAME ,则返回 1 ,否则返回 0

环境处理上的另一个区别是,csh 和 tcsh 为变量和环境值维护着不同的命名空间。如果两个位置出现同一个名称,则变量定义优先。与 bash 一样, unset 命令取消设置一个变量。可使用 unsetenv 命令取消设置环境变量。 取消设置 csh 和 tcsh 变量和环境值 展示了这些命令。

取消设置 csh 和 tcsh 变量和环境值
[ian@atticf22 ~]$ echo "$E1 / $V2"
Env variable 1 / variable 2
[ian@atticf22 ~]$ set E1="I'm now a regular variable"
[ian@atticf22 ~]$ echo $E1
I'm now a regular variable
[ian@atticf22 ~]$ unset E1
[ian@atticf22 ~]$ echo $E1
Env variable 1
[ian@atticf22 ~]$ unsetenv E1
[ian@atticf22 ~]$ echo $E1
E1: Undefined variable.

配置文件

在登录到 Linux 系统时,您的 ID 有一个默认 shell,这就是您的 登录 shell 。该 shell 程序在 /etc/passwd 文件中您的条目中指定,这是用于设置 SHELL 环境变量的值。可使用 man 5 passwd 命令进一步了解 /etc/passwd 文件。

如果您的登录 shell 是 bash,它会在您获得控制权之前运行多个配置文件脚本。如果 /etc/profile 存在,bash 会首先运行它,以及 /etc/profile.d 中的任何 .sh 文件。bash 运行系统脚本后,它会按顺序检查您的主目录中的 ~/.bash_profile、~/.bash_login 和 ~/.profile 文件,并运行它找到的第一个文件。

/etc 中的发行版文件(比如 /etc/profile)可能因为系统更新而修改,所以不要直接编辑它们。如果需要为系统上的所有用户自定义登录环境,可以在 /etc/profile.d 中创建额外的文件。不用说,您可以修改主目录中的文件来适合您的个人工作风格。

登录并使用 bash 后,您可能会启动更多 shell 来运行命令。如果 shell 接受来自键盘的命令,那么它就是 交互式 shell。如果 shell 输入来自文件,那么它就是 非交互式 shell。对于交互式 shell,bash 运行 ~/.bashrc 脚本(如果存在)。您应该习惯在 ~/.bash_profile 中检查此脚本,以便您可在登录时或启动一个交互式 shell 时运行它。取决于您的发行版,您的 ~/.bashrc 可反过来从 /etc/bash.bashrc 或 /etc/bashrc 获取常见别名和全局变量。当您注销时,bash 会运行您的主目录中的 ~/.bash_logout 脚本(如果该脚本存在)。我的 .bash_profile 如 我的 .bash_profile 所示。

我的 .bash_profile
 [ian@atticf22 ~]$ cat ~/.bash_profile
 # .bash_profile

 # Get the aliases and functions
 if [ -f ~/.bashrc ]; then
   . ~/.bashrc
 fi

 # User specific environment and startup programs

 PATH=$PATH:$HOME/.local/bin:$HOME/bin

 export PATH

--login 选项会强制 bash 读取配置文件,就像它是登录 shell 一样, --noprofile 选项会告诉 bash 跳过配置文件。类似地, --norc 选项会禁止对交互式 shell 执行 ~/.bashrc 文件。要强制 bash 使用 ~/.bashrc 以外的文件,可指定 --rcfile 选项和您想要使用的文件名称。 使用一个文件和 --rcfile 参数 展示了如何创建一个简单的示例文件来用于 --rcfile 选项。

使用一个文件和 --rcfile 参数
 [ian@atticf22 ~]$ echo VAR1=var1>testrc
 [ian@atticf22 ~]$ echo $VAR1

 [ian@atticf22 ~]$ bash --rcfile testrc
 bash-4.3$ echo $VAR1
 var1
 bash-4.3$ exit
 exit

重要发现

许多程序(包括 bash)使用 readline 库从键盘读取命令。您可以自定义如何使用内置 bash bind 命令来揭示击键或组合键。例如, bind '"\C-t":"pwd\n"' 将 Ctrl-t(同时按下 Ctrl 和 t)与运行 pwd 命令的行为相绑定。

默认键绑定的全局文件位于 /etc/inputrc。您可以在主目录中的 .inputrc 文件中创建自己的绑定。如果这么做,可包含以下行,以防遗漏全局定义:

$include /etc/inputrc

请参阅 bash 信息页面了解更多信息。

其他启动 bash 的方式

您已经了解了如何启动登录和交互式 shell。当您运行 shell 脚本时,bash 会在非交互式 shell 中启动该脚本,不会读取配置文件。但是,如果设置了 BASH_ENV 变量,bash 会扩展该值,并假设它是一个文件的名称。如果该文件存在,bash 会在运行您在非交互式 shell 中运行的任何脚本或命令之前运行它。 使用 BASH_ENV 演示了这一行为。

使用 BASH_ENV
[ian@atticf22 ~]$ cat testenv.sh
#!/bin/bash
echo "Test of BASH_ENV"
export VAR_ENV="my value"
[ian@atticf22 ~]$ cat testscript.sh
#!/bin/bash
echo "Running $0"
echo "VAR_ENV is set to $VAR_ENV"
[ian@atticf22 ~]$ ./testscript.sh
Running ./testscript.sh
VAR_ENV is set to
[ian@atticf22 ~]$ export BASH_ENV="~/testenv.sh"
[ian@atticf22 ~]$ ./testscript.sh
Test of BASH_ENV
Running ./testscript.sh
VAR_ENV is set to my value

如果某个脚本在子 shell 中运行,当它返回到父 shell 时,它导出的所有变量都会丢失。如果 .bash_profile 运行 ~/.bashrc 脚本,为什么从 ~/.bashrc 导出的变量和函数不会丢失?原因是,您使用 source (或 . )命令在当前环境中运行该脚本。请参阅 我的 .bash_profile 查看示例。

我之前提到过 POSIX。可使用 --posix 选项启动 bash,使其更符合 POSIX 标准。如果您尝试开发的代码将跨具有不同 POSIX 遵从水平的系统而运行,那么您可以这样做。此模式的启动类似于非交互式 shell 的启动,但要运行的初始文件(如果有)是通过 ENV 环境变量确定的,而不是通过 BASH_ENV 变量。

Linux 系统通常拥有 /bin/sh 作为 bash 的符号链接。当 bash 检测到它以名称 sh 运行时,它会尝试遵循更古老的 Bourne shell 的启动行为,但仍符合 POSIX 标准。运行登录 shell 时,bash 尝试读取和运行 /etc/profile 和 ~/.profile。当通过 sh 命令以交互式 shell 形式运行时,bash 会尝试运行 ENV 变量指定的文件,就像在 POSIX 模式下调用它时一样。当作为 sh 交互式地运行时,bash 仅使用 ENV 变量指定的文件;忽略 --rcfile 选项。

最后,如果通过远程 shell 守护进程调用 bash,它将具有交互式 shell 的行为,使用 ~/.bashrc 文件(如果存在)。

管道和命令列表

继续学习别名和函数之前,您需要非常了解管道和命令序列(或列表)。请参阅 “学习 Linux,101:流、管道和重定向 ” 来复习管道概念,参阅 “学习 Linux,101:Linux 命令行 ” 来复习列表或序列。

有时,您必须将命令分组到管道的某个阶段,然后将所有这些命令的输出重定向到管道的下一个阶段。可以使用圆括号在子 shell 中运行命令列表,或者可以使用花括号在父 shell 中运行命令列表。花括号必须使用空格与其他内容分开,而圆括号被识别为运算符,且无需分开。 使用圆括号和花括号对命令分组 使用这两种方法查找 /usr/bin 中最大的文件,以及 /usr/bin 目录使用的空间总量。

使用圆括号和花括号对命令分组
[ian@atticf22 ~]$ (ls -lrS /usr/bin; du /usr/bin) | tail -n 3
-rwxr-xr-x.   1 root root    10669512 Sep 18 10:41 mysql
-rwxr-xr-t.   1 root root    15148768 Sep 14 05:11 emacs-24.5
386296   /usr/bin
[ian@atticf22 ~]$ { ls -lrS /usr/bin; du /usr/bin; } | tail -n 3
-rwxr-xr-x.   1 root root    10669512 Sep 18 10:41 mysql
-rwxr-xr-t.   1 root root    15148768 Sep 14 05:11 emacs-24.5
386296   /usr/bin

Shell 别名

在 bash 中,可为命令定义别名。可使用别名提供命令的替代性名称,提供命令的默认参数,或者有时会构造一个新的或更复杂的命令。可使用 alias 命令设置别名或列出别名,使用 unalias 命令删除它们。 aliasunalias 命令的简单用法 给出了一些例子。

aliasunalias 命令的简单用法
[ian@atticf22 ~]$ # Two commonly set aliases
[ian@atticf22 ~]$ alias ls ll
alias ls='ls --color=auto'
alias ll='ls -l --color=auto'
[ian@atticf22 ~]$ # Add one for a long listing in reverse chronological order
[ian@atticf22 ~]$ alias llrt='ls -lrt --color=auto'
[ian@atticf22 ~]$ llrt /bin
lrwxrwxrwx. 1 root root 7 Aug 16  2014 /bin -> usr/bin
[ian@atticf22 ~]$ # Remove our new alias
[ian@atticf22 ~]$ unalias llrt
[ian@atticf22 ~]$

来自 llrt 命令的多色输出 (来自 使用 BASH_ENV 的屏幕截图)显示了新 llrt 别名的多色输出。

来自 llrt 命令的多色输出

该屏幕截图中使用不同颜色表示来自前一个清单的文本

如果命令的第一个单词未包含在引号中,Bash 别名扩展会在这个单词上进行。如果扩展后的第一个单词是尚未扩展的未加引号的单词,bash 会再次执行别名扩展,依此类推。所以 llrt 别名可能是 alias llrt='ls -lrtllrt 别名也可能跳过 --color=auto 参数 — 因为两个别名都会进一步替换单词 ls 。您可以试验这些和其他变形。

别名的另一个常见用法是用于根用户。 cprmmv 命令通常别名化来包含 -i 参数,以帮助预防意外破坏文件。您可以检查您自己的系统上的这些别名。

作为最后一个且更复杂的示例,看看 which 命令的别名 中是如何别名化 which 命令的。

which 命令的别名
[ian@atticf22 ~]$ alias which
alias which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias \
--read-functions --show-tilde --show-dot'
[ian@atticf22 ~]$ which which
alias which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias \
--read-functions --show-tilde --show-dot'
  /usr/bin/which

在这里, aliasdeclare -f 命令使用圆括号分组到一起,在一个子 shell 中运行。这个分组列出该 shell 已知的别名和函数。该分组的输出被传输到 /usr/bin/which 可执行文件。如果该可执行文件在终端上运行( --tty-only 选项),/usr/bin/which 会从 stdin 读取别名( --read-alias 选项)和函数( --read-functions 选项)定义,并包含任何匹配的别名或函数的输出,以及在当前 PATH 上找到的可执行文件的基本输出。请参阅 which 的手册页,进一步了解其他选项。

别名、命令列表和引号

在定义使用命令列表或其他 shell 元字符的别名时需要小心。如果想在别名中使用 shell 变量,还需要小心使用引号。

which 命令的别名 中, which 的别名使用一个命令列表和单引号。现在,假设您希望一个名为 lsdu 的命令列出当前目录的内容,以及它和它的所有子目录使用的空间量。 在别名中使用命令列表和引号的正确和错误方式 给出了创建别名的正确和错误方式。

在别名中使用命令列表和引号的正确和错误方式
[ian@atticf22 developerworks]$ # Wrong way
[ian@atticf22 developerworks]$ alias lsdu=ls;du -sh
8.2M    .
[ian@atticf22 developerworks]$ lsdu
new-article.py        new-article.vbs  schema     tools        web
new-article-rich.vbs  readme           templates  validate.py  xsl
[ian@atticf22 developerworks]$ # Right way
[ian@atticf22 developerworks]$ alias lsdu='ls;du -sh'
[ian@atticf22 developerworks]$ lsdu
new-article.py        new-article.vbs  schema     tools        web
new-article-rich.vbs  readme           templates  validate.py  xsl
8.2M    .

在错误方式示例中, alias 命令仅包含一直到 ; 元字符的文本,该行的剩余部分会立即执行。在类似这样的情况下,您需要为组成别名的全部文本加上引号。但是您应使用单引号还是双引号?

如果您希望 shell 在定义别名时计算所有 shell 变量引用,则使用双引号。如果您希望在执行别名时计算引用,则使用单引号。 在别名中使用单引号和双引号 使用 PWD (打印工作目录)变量演示了区别。

在别名中使用单引号和双引号
[ian@atticf22 ~]$ alias mypwd1='echo Current directory is $PWD'
[ian@atticf22 ~]$ alias mypwd2="echo Current directory is $PWD"
[ian@atticf22 ~]$ # cd to a different directory
[ian@atticf22 ~]$ cd developerworks
[ian@atticf22 developerworks]$ mypwd1
Current directory is /home/ian/developerworks
[ian@atticf22 developerworks]$ mypwd2
Current directory is /home/ian
[ian@atticf22 developerworks]$ alias mypwd1 mypwd2
alias mypwd1='echo Current directory is $PWD'
alias mypwd2='echo Current directory is /home/ian'

Shell 函数

别名很有用,但如果您想处理参数,会发生什么情况?别名仅扩展第一个单词,命令行上的剩余所有内容会附加到扩展结果上。如果您想使用一些参数来运行命令,然后对输出进行处理,则不能使用别名。在这种情况下,需要使用一个 shell 函数。

Shell 函数与别名相比有两个优势:

  • 可以处理参数。
  • 可以使用编程结构(比如测试和循环)来增强处理。

本教程仅介绍简单的函数。本系列的下一篇教程将介绍可在 shell 函数和脚本中使用的增强的编程结构。在这里,我将介绍如何构建必要的命令,然后如何将它们放入一个函数中。

假设您想要一个名为 ldirs 的命令,它的行为非常像 ls ,但仅列出目录。您首先想到的可能是最初有一个长清单,过滤以 d 开头的行,然后执行进一步处理来消除额外的信息。 首次尝试过滤目录 显示了这种想法的第一个阶段。

首次尝试过滤目录
[ian@atticf22 ~]$ ls -l developerworks/ | grep "^d"
drwxrwxr-x. 2 ian ian  4096 Nov 13 13:06 my first article
drwxrwxr-x. 2 ian ian  4096 Sep 12  2014 readme
drwxrwxr-x. 4 ian ian  4096 Sep 12  2014 schema
drwxrwxr-x. 3 ian ian  4096 Sep 12  2014 templates
drwxrwxr-x. 3 ian ian  4096 Sep 12  2014 tools
drwxrwxr-x. 3 ian ian  4096 Mar  5  2013 web
drwxrwxr-x. 4 ian ian  4096 Sep 12  2014 xsl

显然,它不会仅从输出中剥离最后一个单词,因为一些目录名称可能包含空格。与使用长清单相比,更好的方法是从一个分类清单入手,该清单中的目录名称末尾处有一个 / 字符。 ls 输出分类以查找目录 展示了这种方法的可能用法。

ls 输出分类以查找目录
[ian@atticf22 ~]$ ls -F developerworks/ | grep "/$"
my first article/
readme/
schema/
templates/
tools/
web/
xsl/

效果好得多了。现在仅剩下剥离结尾的 /,这可使用 sed 轻松完成,如 一个 ldirs 命令列表示例 所示。

一个 ldirs 命令列表示例
[ian@atticf22 ~]$ ls -F developerworks/ | grep "/$" | sed -e 's/\/$//'
my first article
readme
schema
templates
tools
web
xsl

现在您拥有 ldirs 函数所需的复杂命令,是时候学习如何将它变成函数了。函数包含一个名称,后跟 () ,然后是一个复合命令。对于本教程,复合命令是任何以分号终止且放在花括号(必须使用空格将它们与其他记号分开)中的命令或命令列表。本系列的下一篇教程将介绍更复杂的复合命令。

在 bash shell 中,函数名称前可添加单词 function 作为前缀,但这不是 POSIX 规范的一部分,而且不受 dash 等更简洁的 shell 支持。另一篇教程将介绍如何确保一个脚本由特定 shell 解释,即使您通常使用一个不同的 shell。

首次尝试过滤目录ls 输出分类以查找目录一个 ldirs 命令列表示例 中的命令列出了我系统上的一个名为 developerworks 的目录,但您可能不希望每次都列出同一个目录 — 而且在使用别名时,不能将目录名称替换为参数。所以您会选择使用函数。

编写函数前,您需要知道如何使用参数。在函数内,使用 Shell 函数参数 中的 bash 特殊变量来引用参数。与其他 shell 变量一样,为这些变量添加一个 $ 符号作为前缀来引用它们。

Shell 函数参数
参数 用途
012 ,等等 从参数 0 开始的位置参数。参数 0 指的是启动 bash 的程序名称,或者如果该函数在 shell 脚本内运行,那么该参数指的是 shell 脚本的名称。请参阅 bash 手册页了解其他可能性,比如在使用 -c 参数启动 bash 时。放在单引号或双引号内的字符串作为单个参数传递,而且会剥离引号。如果使用双引号,在调用函数之前会扩展所有 shell 变量,比如 $HOME 。如果传递的参数包含嵌入的空白或其他可能对 shell 具有特殊含义的字符,则需要使用单引号或双引号。
* 从参数 1 开始的位置参数。如果在双引号内进行扩展,则扩展结果是一个单词,使用字段间分隔符 (IFS) 特殊变量的第一个字符来分离参数,或者如果 IFS 是空的,则没有中间空格。默认的 IFS 值是一个空白、制表符和换行符。如果 IFS 未设置,则使用的分隔符为空白,与默认 IFS 一样。
@ 从参数 1 开始的位置参数。如果在双引号内进行扩展,则每个参数都会变成一个单词,以便 "$@" 等于 "$1""$2" ,等等,如果您的参数可能包含嵌入的空白,则使用此形式。
# 参数数量,不包含参数 0。

如果拥有 9 个以上的参数,则不能使用 $10 指代第 10 个参数。必须首先处理或保存第一个参数 ($1),然后使用 shift 命令丢弃参数 1 并将所有剩余参数的编号减 1,以便 $10 变成 $9 ,依此类推。 $# 的值会被更新,以反映剩余参数数量。

首先,尝试定义一个简单函数,该函数只会告诉您它拥有多少个参数并显示它们,如 一个显示其参数的简单函数 所示。

一个显示其参数的简单函数
 [ian@atticf22 ~]$ testfunc () { echo "$# parameters"; echo "$@"; }
 [ian@atticf22 ~]$ testfunc
 0 parameters

 [ian@atticf22 ~]$ testfunc a b c
 3 parameters
 a b c
 [ian@atticf22 ~]$ testfunc a "b c"
 2 parameters
 a b c

在这个示例中,可以使用 $*"$*"$@"$@" ,而不会在输出中看到太大区别 — 除了参数数量和处理任何变量引用的方式。当情况变得更复杂时,这些区别肯定会变得很重要。

现在创建您的 ldirs 函数,如 ldirs 函数 所示,使用 一个 ldirs 命令列表示例 中的命令列表。

ldirs 函数
[ian@atticf22 ~]$ # You can enter the function on one line
[ian@atticf22 ~]$ ldirs () { ls -F "$@" |  grep "/$" | sed -e 's/\/$//'; }
[ian@atticf22 ~]$ # Or you can enter it on multiple lines
[ian@atticf22 ~]$ # in which case you do not need the final semi-colon
[ian@atticf22 ~]$ ldirs ()
> {
> ls -F "$@" |  grep "/$" |
> sed -e 's/\/$//'
> }

来自 ldirs 函数的输出 显示了在我的系统上运行 ldirs 来列出 developerWorks XML 写作软件包目录的输出。

来自 ldirs 函数的输出
[ian@atticf22 ~]$ ldirs developerworks/ ~/developerworks/xsl/7.0/
my first article
readme
schema
templates
tools
web
xsl
en_US
en_VN
es_AR
ja_JP
ko_KR
pt_BR
ru_RU
zh_CN
[ian@atticf22 ~]$ ldirs -ilrt developerworks
2511035 drwxrwxr-x. 3 ian ian  4096 Mar  5  2013 web
3019425 drwxrwxr-x. 4 ian ian  4096 Sep 12  2014 xsl
2511022 drwxrwxr-x. 3 ian ian  4096 Sep 12  2014 templates
2511036 drwxrwxr-x. 4 ian ian  4096 Sep 12  2014 schema
3023428 drwxrwxr-x. 3 ian ian  4096 Sep 12  2014 tools
3282516 drwxrwxr-x. 2 ian ian  4096 Sep 12  2014 readme
3277408 drwxrwxr-x. 2 ian ian  4096 Nov 13 13:06 my first article
[ian@atticf22 ~]$ ldirs  developerworks/my\ first\ article/
[ian@atticf22 ~]$ mkdir developerworks/my\ first\ article/"dir example"
[ian@atticf22 ~]$ ldirs  developerworks/my\ first\ article/
dir example

尽管 ldirs 示例非常简单,但它演示了组合使用小构件来创建更复杂的或自定义的命令的技术。管道和列表都是 Linux(或 UNIX)工具箱的基本组成部分。

框架目录

您可能想知道像 ~/.bash_profile、~/.bashrc 或 ~/.bash_logout 这样的文件是如何在您的主目录中创建的。这些是从 /etc/skel 复制过来的框架文件。 /etc/skeleton 的内容 显示了我的系统上的 /etc/skel 中的文件。请注意,您需要使用 ls-a 选项,该目录可能是空的。

/etc/skeleton 的内容
[ian@atticf22 ~]$ ls -a /etc/skel/
.  ..  .bash_logout  .bash_profile  .bashrc  .emacs  .kshrc  .mozilla

使用 -m 选项运行 useradd 命令来创建主目录时,这些文件会复制到新的主目录。在这里可以看到,Firefox 或 Korn shell 等程序可以向 /etc/skel 添加额外的目录。如果您想自定义为新用户创建的目录,可向 /etc/skel 添加您的新目录和任何合适的文件。

/etc/skel 位置可通过 /etc/defaults/useradd 文件进行配置。我的系统上的该文件如 /etc/defaults/useradd 所示。

/etc/defaults/useradd
[ian@atticf22 ~]$ cat /etc/default/useradd
# useradd defaults file
GROUP=100
HOME=/home
INACTIVE=-1
EXPIRE=
SHELL=/bin/bash
SKEL=/etc/skel
CREATE_MAIL_SPOOL=yes

命令搜索路径

您知道,bash 使用 PATH 变量中列出的目录来搜索可执行文件。 PATH 变量包含一个以冒号分隔各个条目的目录列表。根用户的 PATH 通常与普通用户的不同,如 普通用户和根用户的 PATH 所示。

普通用户和根用户的 PATH
[ian@atticf22 ~]$ echo $PATH
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/home/ian/.local/bin:/home/ian/bin
[ian@atticf22 ~]$ su -
Password:
[root@atticf22 ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin

现在假设您的公司有一个程序库 /opt/company-bin,该库位于每个非根用户的路径上。假设您想将该库添加到 PATH 变量前面,但前提是它还未添加到那里。进一步假设您想要一个函数或别名(假设为 add2path ),用户可使用它更轻松地向其 PATH 变量添加更多目录。您将使用目前在这里学到的知识完成这两个任务。

首先,您需要确定哪种选择在这里更合适:别名还是函数。只有在该条目不在 PATH 上时才能添加该条目的事实表明,您需要处理一个参数。别名不会处理参数,所以您需要一个函数。

查看某个目录是否已在您路径上的一种简单方法是,为目录名称和 PATH 变量添加前导和结尾冒号 (:),测试分隔的目录名称是否包含在分隔的 PATH 中。如果不以这种方式分隔,您可以测试 /bin 并实际找到 /usr/bin,其中包含字符串 /bin。使用 shell 括号扩展从参数中删除一个字符串,然后将结果与原始结果进行比较。 测试一个字符串是否包含在另一个字符串中 显示了您可以使用的基本结构。

测试一个字符串是否包含在另一个字符串中
[ian@atticf22 ~]$ # Augment the path
[ian@atticf22 ~]$ augpath=":$PATH:"
[ian@atticf22 ~]$ # demonstrate with /usr/bin
[ian@atticf22 ~]$ augdir=":/usr/bin:"
[ian@atticf22 ~]$ echo -e "$augdir\n$augpath\n${augpath/$augdir}":/usr/bin:
:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/home/ian/.local/bin:/home/ian/bin:
:/usr/local/bin:/usr/local/sbin/usr/sbin:/bin:/sbin:/home/ian/.local/bin:/home/ian/bin:
[ian@atticf22 ~]$ test "$augpath" != "${augpath/$augdir}" && echo Found || echo Not
Found

现在很容易构造 add2path 函数。如果您将中间变量声明为 local ,那么它们不会被函数外部的实体所知。 add2path 函数及其用法 给出了 add2path 函数及其用法的一个示例。

add2path 函数及其用法
[ian@atticf22 ~]$ add2path ()
> {
> local augpath augdir
> augpath=":$PATH:"
> augdir=":$1:"
> test "$augpath" = "${augpath/$augdir}" && PATH="$1:$PATH"
> }
[ian@atticf22 ~]$ add2path /opt/company-bin
[ian@atticf22 ~]$ echo $PATH
/opt/company-bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:
   /sbin:/home/ian/.local/bin:/home/ian/bin

在本教程前面学习了配置文件后,您已经知道了添加此命令序列作为系统级函数的最佳方式是在 /etc/profile.d 中创建一个 company-bin.sh 文件。您需要根权限才能创建此文件。请记住,您需要将 /opt/company-bin 添加到非根用户的 PATH/etc/profile.d/company-bin.sh 文件 展示了该文件的可能形式。

/etc/profile.d/company-bin.sh 文件
 [ian@atticf22 ~]$ cat /etc/profile.d/company-dir.sh
 # Declare the add2path function

 add2path ()
 {
    local augpath augdir
    augpath=":$PATH:"
    augdir=":$1:"
    test "$augpath" = "${augpath/$augdir}" && PATH="$1:$PATH"
 }

 # Add /opt/company-bin if not already on non-root PATH
 [ $(id -u) -ne 0 ] && add2path /opt/company-bin

在您自己的系统上尝试这个示例。

对使用和自定义 bash shell 环境的介绍到此结束。

本文翻译自:Learn Linux, 101: Customize and use the shell environment(2015-12-12)