0%

作为Linux命令三剑客之一的awk功能非常强大,用法也较为复杂,最擅长是按列提取值,awk结合grep结合xargs可以完成非常多的事,网上教程很多,我就以解析一个csv文件为例演示awk最实用的一些功能。

test.csv内容

姓名 成绩
张三 95
李四
王五 98
平均分 96.5

文本内容

1
2
3
4
5
姓名,成绩
张三,95
李四,
王五,98
平均分,96.5

需求

打印除了表头(第一行)和汇总(最后一行)外所有行的第一列和最后一列,但如果最后一列是空的则这行不打印。
对于上例,打印出来应该是

1
2
张三 95
王五 98

处理语句

1
cat test.csv|awk 'NR>1'|awk 'NR!=1 {print prev} {prev=$0}'|awk -F, '$NF ~ /[^\t\r\n ]/ {print $1,$NF}'

其中用到了3次awk:
第一个awk,awk 'NR>1',实现去掉首行
第二个awk,awk 'NR!=1 {print prev} {prev=$0}',实现去掉最后一行
第三个awk,awk -F, '$NF ~ /[^\t\r\n ]/ {print $1,$NF}',指定了,为分隔符,当最后一列不空时打印第一列和最后一列

awk命令格式

awk

options

最长用的就是 -F 用来指定列分割符

pattern & action

类似if条件,可以用()括起来也可以不括,多个条件&&连接,多个条件||连接,只有满足条件才会做后面的动作,省略pattern相当于条件为真,即对每一行做action动作。action可以和{}一起省略,省略时执行默认的action输出整行。action可以是多条语句,语句间用;分割。
pattern {action}整体可以重复多次,表示多一行执行多组pattern {action}

在pattern和action中可以使用的特殊符号:

行号:NR
列数:NF
NF是一个数字,而$NF是最后一列的值。

file

即需要处理文件的文件名,通常使用awk处理来自管道的文本,所以file通常是省略的。

一个实用的例子

获取各个网口配置的IPv4

1
ifconfig|awk 'prev ~ /.+:/ && $1=="inet" {print prev,$2} {prev=$1}'

ifconfig的输出类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.1.242 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::20c:29ff:fe7d:e0 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:7d:00:e0 txqueuelen 1000 (Ethernet)
RX packets 523230074 bytes 791300064486 (791.3 GB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 17037016 bytes 1268820578 (1.2 GB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 138 bytes 13090 (13.0 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 138 bytes 13090 (13.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

处理后的输出为

1
2
ens33: 192.168.1.242
lo: 127.0.0.1

默认情况Linux添加用户需要等待New password:提示在输入密码,无法通过一句命令直接添加用户并设置密码,这导致在Shell脚本中添加用户或者批量添加变得困难,我们可以通过expect自制一个非交互式的添加用户并设置密码的命令。

expect

expect是一直可执行工具,通过执行expect脚本来把shell交互的行为提前设置在expect脚本中。

如果没有安装expect,首先安装expect

1
apt intall expect

expect脚本主要有spawn/expect/send三个命令 :

  • spawn后面跟着会产生交互行为的命令
  • expect后面是等待命令产生的交互提示
  • send后面是等到交互提示后输入的字符

非交互添加用户脚本

1
2
3
4
5
6
7
8
9
#!/usr/bin/expect
set username [lindex $argv 0]
set password [lindex $argv 1]
spawn adduser --gecos "" $username
expect "*password"
send "$password\n"
expect "*password"
send "$password\n"
expect eof

例如保存成 adduserwithpw.sh,执行./adduserwithpw.sh user1 pass就可以添加名为user1密码为pass的用户了。

官网 https://grpc.io/
中文文档 http://doc.oschina.net/grpc/

环境搭建

安装依赖

1
apt install -y build-essential autoconf libtool pkg-config cmake

Clone gRPC repo

1
git clone --recurse-submodules -b v1.56.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc

C++ 构建并安装gRPC和Protocol Buffers

1
2
3
4
5
6
7
8
9
10
11
12
13
export MY_INSTALL_DIR=$HOME/.local
mkdir -p $MY_INSTALL_DIR
export PATH="$MY_INSTALL_DIR/bin:$PATH"

mkdir -p cmake/build
pushd cmake/build
cmake -DgRPC_INSTALL=ON \
-DgRPC_BUILD_TESTS=OFF \
-DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR \
../..
make -j 4
make install
popd

这里我们安装了gRPC的头文件和库文件,以及一个工具protoc

Python 安装gRPC和gRPC工具(包含 protoc

1
2
pip install grpcio
pip install grpcio-tools

定义接口和数据

.proto文件

.proto文件用于定义数据(请求参数message和返回值message)和接口(service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
syntax = "proto3"; // 表示这个文件遵循的proto语法的版本

package calc; // 对C++来说表示生成代码的namespace,如果不指定则为全局

// 定义服务接口
service Calc {
rpc Add (AddRequest) returns (AddReply) {}
}

// 定义Add方法的请求参数
message AddRequest {
int32 a = 1;
int32 b = 2;
}

// 定义Add方法的返回数据
message AddReply {
int32 sum = 1;
}

从.proto文件生成代码

protoc工具用于将使用ProtoBuf定义的.proto文件编译成特定编程语言的源代码。
.proto文件描述了数据和接口的定义。
例如通过.proto生成C++代码的示例:

1
protoc --cpp_out=./output_directory ./protos/proto_file.proto

会在output_directory中生成一个protos文件夹,里面有.proto同名的*.pb.h*.pb.cc文件。
这里有一个细节,生成protos文件夹是因为指定的.proto文件带有路径,生成的路径和指定.proto的路径相同,如果想把.h和.cc文件直接生成在指定的文件夹中,可以通过-I参数指定.proto文件的路径而不在指定.proto文件时携带路径。即

1
protoc --cpp_out=./output_directory -I./protos proto_file.proto

如果使用cmake构建项目,通常将这个生成过程写在CMakeList.txt中,用add_custom_command命令完成。
这里生成的.h和.cc中只包含了对数据(.proto中的message)的封装,而不包含接口(.proto中的service)的黏合剂,要生成service的代码需要利用grpc_cpp_plugin,可以通过在protoc命令中增加参数一并生成接口和数据的代码。

1
protoc --cpp_out=./output_directory --grpc_out=./output_directory --plugin=protoc-gen-grpc=/bin/grpc_cpp_plugin -I./protos proto_file.proto

会在output_directory中生成.proto同名的*.pb.h*.pb.cc文件,以及*.grpc.pb.h*.grpc.pb.cc*.grpc.pb.*里面就包含了根据service生成的代码。

Python版的protoc是已模块的形式安装的,使用方法

1
python -m grpc_tools.protoc --python_out=./output_directory --pyi_out=./output_directory --grpc_out=./output_directory --plugin=protoc-gen-grpc=/bin/grpc_python_plugin -I./protos proto_file.proto

另外其实上面通过C源码编译安装的protoc工具也是支持生成python代码的,但python的protoc模块不能生成C版本的代码。

项目文件存放结构

不是必须的,我感觉这样存放会比较方便,protosprotos_gen是客户端和服务器共用的,而且必须一致,所以只存统一的一份,这里的protos_gen就是由protos生成的意思也就是上面的output_directory目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── calc_client
│ ├── CMakeLists.txt
│ └── src
│ └── main.cpp
├── calc_server
│ ├── CMakeLists.txt
│ └── src
│ └── main.cpp
├── protos
│ └── calc.proto
└── protos_gen
├── calc.grpc.pb.cc
├── calc.grpc.pb.h
├── calc.pb.cc
└── calc.pb.h

CMake构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
cmake_minimum_required(VERSION 3.10)
project(calc_server CXX C)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")

set(CMAKE_C_FLAGS_DEBUG " -std=c99 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")
set(CMAKE_CXX_FLAGS_DEBUG " -std=c++17 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")

set(CMAKE_C_FLAGS_RELEASE " -std=c99 -O3 -Wall -Wno-unused-function -fpic -fPIC")
set(CMAKE_CXX_FLAGS_RELEASE " -std=c++17 -O3 -Wall -Wno-unused-function -fpic -fPIC")

# Find Protobuf installation
# Looks for protobuf-config.cmake file installed by Protobuf's cmake installation.
option(protobuf_MODULE_COMPATIBLE TRUE)
find_package(Protobuf CONFIG REQUIRED)
message(STATUS "Using protobuf ${Protobuf_VERSION}")

# Find gRPC installation
# Looks for gRPCConfig.cmake file installed by gRPC's cmake installation.
find_package(gRPC CONFIG REQUIRED)
message(STATUS "Using gRPC ${gRPC_VERSION}")

include_directories(
/include # 这里替换成gRPC的编译安装目录下的include
include
../protos_gen # 这里是通过protoc生成的文件存放的目录有.h
)
link_directories(
/lib64 # 这里替换成gRPC的编译安装目录下的lib64
lib
)

aux_source_directory(src SRC_LIST)
aux_source_directory(../protos_gen PROTOBUF_SRC_LIST) # 这里是通过protoc生成的文件存放的目录有.cc
add_executable(${PROJECT_NAME}
${SRC_LIST}
${PROTOBUF_SRC_LIST}
)

target_link_libraries(${PROJECT_NAME}
pthread
absl::flags
absl::flags_parse
protobuf::libprotobuf
gRPC::grpc++_reflection
gRPC::grpc++
)

服务端和客户端的CMakeLists.txt都可以套用上面的模板

cpp服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/strings/str_format.h"

#include <grpcpp/ext/proto_server_reflection_plugin.h>
#include <grpcpp/grpcpp.h>
#include <grpcpp/health_check_service_interface.h>

#include "calc.grpc.pb.h"

using namespace std;

// 定义命令行参数,服务器端口号,默认为 50051
ABSL_FLAG(uint16_t, port, 50051, "Server port for the service");

class CalcServiceImpl final : public calc::Calc::Service { // 继承并实现在helloworld.proto中定义的接口
grpc::Status Add(grpc::ServerContext* context, const calc::AddRequest* request, calc::AddReply* reply) override {
// 从request中取参数
auto a = request->a();
auto b = request->b();

// 调用真正的处理逻辑,这里是简单的加法,ServiceImpl可以作为Proxy存在,持有一个业务对象的指针,然后调用相关的处理函数
auto sum = a + b;

// 设置reply中的字段作为返回值
reply->set_sum(sum);
return grpc::Status::OK;
}
};

int main(int argc, char** argv) {
// 解析命令行参数
absl::ParseCommandLine(argc, argv);
uint16_t port = absl::GetFlag(FLAGS_port);
// 构建服务器地址字符串
std::string server_address = absl::StrFormat("0.0.0.0:%d", port);

// 创建服务实现对象
CalcServiceImpl service;

// 启用默认的健康检查服务和反射插件
grpc::EnableDefaultHealthCheckService(true);
grpc::reflection::InitProtoReflectionServerBuilderPlugin();
// 创建服务器构建器
grpc::ServerBuilder builder;

// Listen on the given address without any authentication mechanism.
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());

// 注册服务实现对象到服务器构建器
builder.RegisterService(&service);

// 构建并启动服务器
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;

// 等待服务器关闭
server->Wait();

return 0;
}

在启动时可以指定端口号替换默认的端口号

1
./calc-server --port=12345

cpp客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <memory>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"

#include <grpcpp/grpcpp.h>

#include "calc.grpc.pb.h" // 包含通过 Protocol Buffers 编译生成的 gRPC 服务定义

ABSL_FLAG(std::string, target, "localhost:50051", "Server address");

using namespace std;

class CalcClient {
public:
CalcClient(shared_ptr<grpc::Channel> channel)
: _stub(calc::Calc::NewStub(channel)) {} // 通过通道创建 Stub

// 执行加法操作
int Add(int a, int b) {
// 创建请求
calc::AddRequest request;
request.set_a(a);
request.set_b(b);

// 创建响应容器
calc::AddReply reply;

// 创建客户端上下文
grpc::ClientContext context;

// 实际执行 RPC 调用
grpc::Status status = _stub->Add(&context, request, &reply);

// 根据调用状态进行处理
if (status.ok()) {
return reply.sum(); // 返回相加的结果
} else {
cout << status.error_code() << ": " << status.error_message() << endl; // 打印错误信息
return 0;
}
}

private:
unique_ptr<calc::Calc::Stub> _stub; // gRPC Stub
};

int main(int argc, char** argv) {
absl::ParseCommandLine(argc, argv); // 解析命令行参数
string target_str = absl::GetFlag(FLAGS_target); // 获取服务器地址参数
cout << "target_str=" << target_str << endl;

// 创建客户端
CalcClient client(grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials()));

int a, b;
while(cin >> a >> b) { // 从标准输入读取输入并执行加法操作
auto sum = client.Add(a, b);
cout << a << " + " << b << " = " << sum << endl; // 打印结果
}

return 0;
}

同样地,在启动时可以指定服务器IP和端口号替换默认参数

1
./calc-client --target=127.0.0.1:12345

python服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from concurrent import futures

import grpc
import sys
sys.path.append("../protos_gen")
from calc_pb2 import AddReply # protoc生成的代码
from calc_pb2_grpc import CalcServicer, add_CalcServicer_to_server # protoc生成的代码


class Calc(CalcServicer):
def Add(self, request, context):
return AddReply(sum=request.a + request.b)


def main():
port = '50051'
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_CalcServicer_to_server(Calc(), server)
server.add_insecure_port('0.0.0.0:' + port)
server.start()
print("Server started, listening on " + port)
server.wait_for_termination()


if __name__ == '__main__':
main()

python客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import grpc
import sys
sys.path.append("../protos_gen")
from calc_pb2 import AddRequest # protoc生成的代码
from calc_pb2_grpc import CalcStub # protoc生成的代码


class Calc:
def __init__(self, server):
self.channel = grpc.insecure_channel(server)
self.stub = CalcStub(self.channel)

def __del__(self):
del self.stub
self.channel.close()

def Add(self, **kw):
response = self.stub.Add(AddRequest(**kw))
return response.sum


def main():
client = Calc(server='localhost:50051')
s = client.Add(a=1, b=2)
print(f"1+2={s}")


if __name__ == '__main__':
main()

对于新写的代码,最好不要用下面的方式,而是在配置文件中定义BASE_PATH,路径的连接都使用os.path.join,这样代码自然就是跨平台的,对于遗留代码,可以通过下面的to_os_path快速提供平台兼容性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import sys
import os
import re

# 将传入的路径根据当前平台做规范化,以便让路径在Windows和Linux上兼容
# 对于共享或挂载路径,需要共享已经按命名规则挂在到/mnt目录下(在Linux上) 或 已经通过net use建立了连接(在Windows上)
# Windows和Linux上共享或挂载点对应规则:
# \\192.168.1.107\e <=> /mnt/107e
# X: <=> /mnt/X
# \\192.168.1.99\database <=> /mnt/99database
# 用法:
# 将 open(r"\\192.168.1.107\e\NAV\abc.txt")
# 改成 open(to_os_path(r"\\192.168.1.107\e\NAV\abc.txt"))
# 即可做到平台兼容
def to_os_path(path):
if sys.platform == 'linux':
return to_linux_path(path)
else:
return to_windows_path(path)


def to_linux_path(path):
m = re.match(r"\\\\\d+\.\d+\.\d+\.(\d+)\\([^\\])", path)
if m: # 形似一个Windows网络共享地址
return '/mnt/' + m.group(1) + m.group(2) + path[m.span()[1] : ].replace('\\', '/')
m = re.match(r"([a-zA-Z]):\\", path)
if m: # 形似一个Windows本地绝对路径
return '/mnt/' + m.group(1) + '/' + path[m.span()[1] : ].replace('\\', '/')

# 已经是一个Linux的据对路径,或者是Windows或Linux的相对路径
return path.replace('\\', '/')


def to_windows_path(path):
m = re.match(r"/mnt/(\d+)([^\\])", path)
if m:
return r"\\192.168.1.{}\{}{}".format(m.group(1), m.group(2), path[m.span()[1] : ].replace('/', '\\'))
m = re.match(r"/mnt/([a-zA-Z])\b", path)
if m:
return m.group(1) + ":" + path[m.span()[1] : ].replace('/', '\\')
return path.replace('/', '\\')

1
2
3
4
5
6
7
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU]
"DetectionFrequencyEnabled"=dword:00000000
"AutoInstallMinorUpdates"=dword:00000000
"NoAutoRebootWithLoggedOnUsers"=dword:00000001
"NoAutoUpdate"=dword:00000001

保存成.reg装机合并进注册表

搭建squid代理服务器

首先在代理服务器上安装squid
yum install squidapt install squid
编辑
/etc/squid/squid.conf
在上面增加一条acl定义

1
acl trustedhost src 47.103.123.187/32

在下面适当的位置增加一条允许访问

1
2
3
4
#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#
http_access allow trustedhost

确保squid服务器处于运行状态

1
service squid status

如果服务没有运行则启动即可

1
service squid start

如果已经启动则重新加载配置

1
squid -k rec

squid默认运行在3128端口,有必要的话把它通过网关路由器或通过ssh隧道映射到公网。

在Ubuntu上配置apt的代理

编辑/etc/apt/apt.conf.d/proxy.conf文件
添加条目

1
2
Acquire::http::Proxy "http://proxy-IP-address:proxyport/";
Acquire::https::Proxy "http://proxy-IP-address:proxyport/";

在CentOS上配置yum的代理

编辑/etc/yum.conf文件
添加条目

1
http_proxy=http://proxy-IP-address:proxyport

Linux上

在Linux上我们可以很方便地实现端口转发,比如通过SSH-Tunnel,使用

1
ssh -CfNg -L 监听IP:监听端口:目标IP:目标端口 localhost

甚至可以通过目标局域网暴露出来的一个ssh端口转发到另一个局域网的内部的服务器上,更详细的参考另一篇之前写的文章ssh-tunnel

通过ssh开启的端口转发在系统重启后需要重新建立,而且如果需要建立很多端口转发这样不方便管理,这时候我们可以使用rinetd工具

apt install rinetd会以服务的形式安装rinetd

service rinetd status可以查看运行状态

配置文件在/etc/rinetd.conf

修改配置后可以通过service rinetd reload重新加载配置而不必重启服务

日志文件在/var/log/rinetd.log

Windows上

在Windows上其实也有个内置的工具可以完成端口转发,而且也很方便

开启端口转发

1
netsh interface portproxy add v4tov4 listenaddress=监听IP listenport=监听端口 connectaddress=目标IP connectport=目标端口

其中监听IP可以填0.0.0.0表示接收来着任意网卡接口的数据。

查看端口转发

1
netsh interface portproxy show v4tov4

重启后也会保留设置,除非主动关闭

关闭端口转发

1
netsh interface portproxy delete v4tov4 listenaddress=本地IP listenport=本地端口

Ubuntu

1
cat /var/log/syslog | grep cron

修改rsyslog配置

1
vim /etc/rsyslog.d/50-default.conf

解开注释

1
cron.* /var/log/cron.log

重启rsyslog

1
service rsyslog restart

查看crontab日志

1
tail -f /var/log/cron.log

CentOS

1
vim /var/log/cron
1
vim /var/spool/mail/root

获取当前日期

1
date "+%Y-%m-%d"

获取当前时间

1
date "+%H:%M:%S"

获取1天前的日期

1
date -d"-1 day" "+%Y-%m-%d"

获取1个月前的日期

1
date -d"-1 months" "+%Y-%m-%d"

获取1小时前的日期时间

1
date -d"-1 hours" "+%Y-%m-%d %H:%M:%S"

备份命令

1
mongodump -h <ip>:<port> -d <数据库名> -u root -p pass -o <备份目录> [--gzip]

例如

1
2
TRADE_DATE=`date "+%Y%m%d"`
mongodump -h 127.0.0.1:27017 -d ftresearch -u root -p pass -o /var/lib/mongodb/backup/$TRADE_DATE --gzip

其中--gzip表示压缩备份文件,对于压缩

还原备份

还原前我们先启动一个新的MongoDB的容器,把容器的27017端口映射到宿主机的47017。

1
docker run -dit -p 47017:27017 --name=mongo_test -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=pass mongo:3.6.2

执行还原命令

1
mongorestore -h <ip>:<port> -d <数据库名> -u root -p pass <备份目录> --drop --gzip

例如

1
mongorestore -h 127.0.0.1:47017 -u root -p pass /var/lib/mongodb/backup/20230709 [--drop] [--gzip]

其中--drop的意思是如果要还原的数据库已存在,则先删除。
--gzip表示还原的是压缩备份,如果备份时没加--gzip参数则还原时也不加。