愚蠢的 Bash 技巧:历史记录、重用参数、文件和目录、函数等
作为系统管理员,shell 是日常操作的一部分。shell 通常比图形用户界面 (GUI) 提供更多选项和灵活性。日常重复任务可以通过脚本轻松自动执行,或者可以安排任务在一天中的特定时间运行。shell 提供了一种与系统交互的便捷方式,使您能够在更短的时间内完成更多工作。有许多不同的 shell,包括 Bash、zsh、tcsh 和 PowerShell。
在这篇由两部分组成的博客文章中,我分享了一些我用来加快工作速度并留出更多时间喝咖啡的 Bash 单行代码。在这篇第一篇博文中,我将介绍历史记录、最后一个参数、处理文件和目录、读取文件内容和 Bash 函数。在第二部分中,我将研究 shell 变量、find 命令、文件描述符和远程执行操作。
使用 history 命令
该history
命令非常方便。History
它允许我查看在特定系统上运行了哪些命令或向该命令传递了哪些参数。我用它history
来重新运行命令而不必记住任何内容。
最近命令的记录默认存储在 中。~/.bash_history.
可以通过修改 HISTFILE shell 变量来更改此位置。还有其他变量,例如 HISTSIZE(当前会话在内存中存储的行数)和 HISTFILESIZE(在历史文件中保留多少行)。如果您想了解有关 的更多信息history
,请参阅man bash
。
假设我运行以下命令:
$> sudo systemctl status sshd
Bash 告诉我 sshd 服务未运行,因此我接下来要做的是启动该服务。我已使用上一个命令检查了其状态。该命令已保存在 中history
,因此我可以引用它。我只需运行:
$> !!:s/status/start/
sudo systemctl start sshd
上述表达式有以下内容:
- !!-重复历史记录中的最后一条命令
- :s/status/start/ -用start替换status
结果是sshd服务启动了。
接下来,我使用以下命令将默认的 HISTSIZE 值从 500 增加到 5000:
$> echo “HISTSIZE=5000” >> ~/.bashrc && source ~/.bashrc
如果我想显示历史记录中的最后三个命令怎么办?我输入:
$> history 3
1002 ls
1003 tail audit.log
1004 history 3
我通过参考历史行号来运行。在本例中,我使用第 1003 行tail
:audit.log
$> !1003
tail audit.log
..
..
Imagine you've copied something from another terminal or your browser and you accidentally paste the copy (which you have in the copy buffer) into the terminal. Those lines will be stored in the history, which here is something you don't want. So that's where unset HISTFILE && exit comes in handy
$> unset HISTFILE && exit
or
$> kill -9 $$
Reference the last argument of the previous command
When I want to list directory contents for different directories, I may change between directories quite often. There is a nice trick you can use to refer to the last argument of the previous command. For example:
$> pwd
/home/username/
$> ls some/very/long/path/to/some/directory
foo-file bar-file baz-file
In the above example, /some/very/long/path/to/some/directory
is the last argument of the previous command.
If I want to cd
(change directory) to that location, I enter something like this:
$> cd $_
$> pwd
/home/username/some/very/long/path/to/some/directory
Now simply use a dash character to go back to where I was:
$> cd -
$> pwd
/home/username/
Work on files and directories
Imagine that I want to create a directory structure and move a bunch of files having different extensions to these directories.
First, I create the directories in one go:
$> mkdir -v dir_{rpm,txt,zip,pdf}
mkdir: created directory 'dir_rpm'
mkdir: created directory 'dir_txt'
mkdir: created directory 'dir_zip'
mkdir: created directory 'dir_pdf'
Next, I move the files based on the file extension to each directory:
$> mv -- *.rpm dir_rpm/
$> mv -- *.pdf dir_pdf/
$> mv -- *.txt dir_txt/
$> mv -- *.zip dir_txt/
The double dash characters --
mean End of Options. This flag prevents files that begin with a dash from being treated as arguments.
Next, I want to replace/move all *.txt files to *.log files, so I enter:
$> for f in ./*.txt; do mv -v ”$file” ”${file%.*}.log”; done
renamed './file10.txt' -> './file10.log'
renamed './file1.txt' -> './file1.log'
renamed './file2.txt' -> './file2.log'
renamed './file3.txt' -> './file3.log'
renamed './file4.txt' -> './file4.log'
Instead of using the for
loop above, I can install the prename
command and accomplish the above goal like this:
$> prename -v 's/.txt/.log/' *.txt
file10.txt -> file10.log
file1.txt -> file1.log
file2.txt -> file2.log
file3.txt -> file3.log
file4.txt -> file4.log
Often, when modifying a configuration file, I make a backup copy of the original one by using a basic copy command. For example:
$> cp /etc/sysconfig/network-scripts/ifcfg-eth0 /etc/sysconfig/network-scripts/ifcfg-eth0.back
As you can see, repeating the whole path and appending .back to the file isn't that efficient and probably error-prone. There is a shorter, neater way to do this. Here it comes:
$> cp /etc/sysconfig/network-scripts/ifcfg-eth0{,.back}
You can perform different checks on files or variables. Run help test
for more information.
Use the following command to discover if a file is a symbolic link:
$> [[ -L /path/to/file ]] && echo “File is a symlink”
Here is an issue I ran across recently. I wanted to gunzip/untar a bunch of files in one go. Without thinking, I typed:
$> tar zxvf *.gz
The result was:
tar: openvpn.tar.gz: Not found in archive
tar: Exiting with failure status due to previous errors
The tar files were:
iptables.tar.gz
openvpn.tar.gz
…..
Why didn't it work, and why would ls -l *.gz
work instead? Under the hood, it looks like this:
$> tar zxvf *.gz
Is transformed as follows:
$> tar zxvf iptables.tar.gz openvpn.tar.gz
tar: openvpn.tar.gz: Not found in archive
tar: Exiting with failure status due to previous errors
The tar
command expected to find openvpn.tar.gz within iptables.tar.gz. I solved this with a simple for
loop:
$> for f in ./*.gz; do tar zxvf "$f"; done
iptables.log
openvpn.log
I can even generate random passwords by using Bash! Here's an example:
$> alphanum=( {a..z} {A..Z} {0..9} ); for((i=0;i<=${#alphanum[@]};i++)); do printf '%s' "${alphanum[@]:$((RANDOM%255)):1}"; done; echo
Here is an example that uses OpenSSL:
$> openssl rand -base64 12
JdDcLJEAkbcZfDYQ
Read a file line by line
Assume I have a file with a lot of IP addresses and want to operate on those IP addresses. For example, I want to run dig
to retrieve reverse-DNS information for the IP addresses listed in the file. I also want to skip IP addresses that start with a comment (# or hashtag).
I'll use fileA as an example. Its contents are:
10.10.12.13 some ip in dc1
10.10.12.14 another ip in dc2
#10.10.12.15 not used IP
10.10.12.16 another IP
I could copy and paste each IP address, and then run dig
manually:
$> dig +short -x 10.10.12.13
Or I could do this:
$> while read -r ip _; do [[ $ip == \#* ]] && continue; dig +short -x "$ip"; done < ipfile
What if I want to swap the columns in fileA? For example, I want to put IP addresses in the right-most column so that fileA looks like this:
some ip in dc1 10.10.12.13
another ip in dc2 10.10.12.14
not used IP #10.10.12.15
another IP 10.10.12.16
I run:
$> while read -r ip rest; do printf '%s %s\n' "$rest" "$ip"; done < fileA
Use Bash functions
Functions in Bash are different from those written in Python, C, awk, or other languages. In Bash, a simple function that accepts one argument and prints "Hello world" would look like this:
func() { local arg=”$1”; echo “$arg” ; }
I can call the function like this:
$> func foo
Sometimes a function invokes itself recursively to perform a certain task. For example:
func() { local arg="$@"; echo "$arg"; f "$arg"; }; f foo bar
This recursion will run forever and utilize a lot of resources. In Bash, you can use FUNCNEST to limit recursion. In the following example, I set FUNCNEST=5 to limit the recursion to five.
func() { local arg="$@"; echo "$arg"; FUNCNEST=5; f "$arg"; }; f foo bar
foo bar
foo bar
foo bar
foo bar
foo bar
bash: f: maximum function nesting level exceeded (5)
Use a function to retrieve the most recent or oldest file
Here is a sample function to display the most recent file in a certain directory:
latest_file()
{
local f latest
for f in "${1:-.}"/*
do
[[ $f -nt $latest ]] && latest="$f"
done
printf '%s\n' "$latest"
}
This function displays the oldest file in a certain directory:
oldest_file()
{
local f oldest
for file in "${1:-.}"/*
do
[[ -z $oldest || $f -ot $oldest ]] && oldest="$f"
done
printf '%s\n' "$oldest"
}
These are just a few examples of how to use functions in Bash without invoking other external commands.
I sometimes find myself typing a command over and over with a lot of parameters. One command I often use is kubectl
(Kubernetes CLI). I am tired of running this long command! Here's the original command:
$> kubectl -n my_namespace get pods
or
$> kubectl -n my_namespace get rc,services
This syntax requires me to manually include -n my_namespace
each time I run the command. There is an easier way to do this using a function:
$> kubectl () { command kubectl -n my_namespace ”$@” ; }
Now I can run kubectl
without having to type -n namespace
each time:
$> kubectl get pods
I can apply the same technique to other commands.
Wrap up
These are just a few excellent tricks that exist for Bash. In part two, I will show some more examples, including the use of find and remote execution. I encourage you to practice these tricks to make your command-line administration tasks easier and more accurate.
[ Free online course: Red Hat Enterprise Linux technical overview. ]