跳到主要内容

· 阅读需 1 分钟

1 运行dozzle

docker run --detach --volume=/var/run/docker.sock:/var/run/docker.sock --net host  amir20/dozzle --addr 127.0.0.1:8080  --base /dockerlogs

2 反向代理

server {
    listen 80;
    server_name xxx;
    client_max_body_size 1G;
    add_header  Access-Control-Allow-Origin "https://xxx";
    add_header  Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header  Access-Control-Allow-Headers "Origin, Authorization, Accept";
    add_header  Access-Control-Allow-Credentials true;

    location ^~ /dockerlogs {
        proxy_pass http://localhost:8080;
    }
}

3 访问

http://x.x.x.x/dockerlogs

· 阅读需 9 分钟

在使用中标麒麟V7Update6版本时,遇到了一个ansible执行报错的问题

问题现象

在中标麒麟(neokylin)系统中部署某服务,使用到了ansible,但是执行时发现有yum模块的task报错如下:

TASK [common : Install basic rpms] **************************************************************************
fatal: [node01]: FAILED! => {"changed": false, "msg": ["Could not detect which major revision of yum is in use, which is required to determine module backend.", "You can manually specify use_backend to tell the module whether to use the yum (yum3) or dnf (yum4) backend})"]}

报错为yum模块无法判断出系统的yum版本,提示需要手工执行yum的use_backend参数。同样的task在原生RHEL7系统执行没有遇到任何问题,看样子调入了中标麒麟的某个坑里。

问题分析

根据报错,很明确是因为ansible无法自动判断出系统使用的yum版本导致,我们知道当ansible中yum模块不指定use_backend参数时,将尝试自动判断,而ansible的setup模块可以获取对应的必要信息, 其中一个变量ansible_pkg_mgr及对应yum后端模块,接下来我们执行setup模块输出ansible_pkg_mgr变量来验证下我们的判断:

# ansible -i hosts node01 -m setup -a "filter=ansible_pkg_mgr"
node01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false
}

果然没有办法获取到ansible_pkg_mgr变量,先看下系统版本信息:

~]# cat /etc/neokylin-release
NeoKylin Linux Advanced Server release V7Update6 (Chromium)

接下来根据报错提示信息找到ansible相关代码,在yum.py中,相关代码如下: ansible/plugins/action/yum.py

        if module not in ["yum", "yum4", "dnf"]:
            facts = self._execute_module(module_name="setup", module_args=dict(filter="ansible_pkg_mgr", gather_subset="!all"), task_vars=task_vars)
            display.debug("Facts %s" % facts)
            module = facts.get("ansible_facts", {}).get("ansible_pkg_mgr", "auto")
            if (not self._task.delegate_to or self._task.delegate_facts) and module != 'auto':
                result['ansible_facts'] = {'pkg_mgr': module}

        if module != "auto":

            if module == "yum4":
                module = "dnf"

            if module not in self._shared_loader_obj.module_loader:
                result.update({'failed': True, 'msg': "Could not find a yum module backend for %s." % module})
            else:
                # run either the yum (yum3) or dnf (yum4) backend module
                new_module_args = self._task.args.copy()
                if 'use_backend' in new_module_args:
                    del new_module_args['use_backend']

                display.vvvv("Running %s as the backend for the yum action plugin" % module)
                result.update(self._execute_module(module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val))
                # Now fall through to cleanup
        else:
            result.update(
                {
                    'failed': True,
                    'msg': ("Could not detect which major revision of yum is in use, which is required to determine module backend.",
                            "You can manually specify use_backend to tell the module whether to use the yum (yum3) or dnf (yum4) backend})"),
                }
            )
            # Now fall through to cleanup

如代码所示,当执行yum未指定use_backend参数时,ansible会执行setup模块并根据ansible_pkg_mgr来自动判断yum的版本,获取不到则会报错,继续看下该参数的获取过程,找到pkg_mgr.py,关键代码如下:

ansible/module_utils/facts/system/pkg_mgr.py

    def collect(self, module=None, collected_facts=None):
        facts_dict = {}
        collected_facts = collected_facts or {}

        pkg_mgr_name = 'unknown'
        for pkg in PKG_MGRS:
            if os.path.exists(pkg['path']):
                pkg_mgr_name = pkg['name']

        # Handle distro family defaults when more than one package manager is
        # installed or available to the distro, the ansible_fact entry should be
        # the default package manager officially supported by the distro.
        if collected_facts['ansible_os_family'] == "RedHat":
            pkg_mgr_name = self._check_rh_versions(pkg_mgr_name, collected_facts)
... ...

 def _check_rh_versions(self, pkg_mgr_name, collected_facts):
        if collected_facts['ansible_distribution'] == 'Fedora':
            if os.path.exists('/run/ostree-booted'):
                return "atomic_container"
            try:
                if int(collected_facts['ansible_distribution_major_version']) < 23:
                    for yum in [pkg_mgr for pkg_mgr in PKG_MGRS if pkg_mgr['name'] == 'yum']:
                        if os.path.exists(yum['path']):
                            pkg_mgr_name = 'yum'
                            break
                else:
                    for dnf in [pkg_mgr for pkg_mgr in PKG_MGRS if pkg_mgr['name'] == 'dnf']:
                        if os.path.exists(dnf['path']):
                            pkg_mgr_name = 'dnf'
                            break
            except ValueError:
                # If there's some new magical Fedora version in the future,
                # just default to dnf
                pkg_mgr_name = 'dnf'
        elif collected_facts['ansible_distribution'] == 'Amazon':
            pkg_mgr_name = 'yum'
        else:
            # If it's not one of the above and it's Red Hat family of distros, assume
            # RHEL or a clone. For versions of RHEL < 8 that Ansible supports, the
            # vendor supported official package manager is 'yum' and in RHEL 8+
            # (as far as we know at the time of this writing) it is 'dnf'.
            # If anyone wants to force a non-official package manager then they
            # can define a provider to either the package or yum action plugins.
            if int(collected_facts['ansible_distribution_major_version']) < 8:
                pkg_mgr_name = 'yum'
            else:
                pkg_mgr_name = 'dnf'
        return pkg_mgr_name

以上代码可以看到当判断系统为红帽系,则会继续判断系统版本信息,当主版本号小于8则使用yum,否则使用dnf,这里我们初步判断为麒麟对系统做了某些修改导致无法获取到主版本号。先执行setup获取发行版代号验证下是否执行了上述逻辑:

# ansible -i hosts node01 -m setup -a "filter=ansible_distribution"
node01 | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "RedHat",
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false
}

# ansible -i hosts node01 -m setup -a "filter=ansible_distribution_major_version"
node01 | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution_major_version": "V7Update6",
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false
}

通过setup模块的输出结果可看到系统是判断为redhat发行版,但是通过ansible_distribution_major_version获取到的发行版主版本号为V7Update6, 而和上面判断yum版本的代码关联起来看就会发现问题所在,int(collected_facts['ansible_distribution_major_version']) < 8 中,ansible_distribution_major_version 变量在其初始化的代码中对应为为distribution_version.split('.')[:2][0]的取值,而当系统中获取到的值是V7Update6时,该显然无法满足转换为int的要求。接下来看下V7Update6这个关键字的定义位置,根据经验系统版本相关信息应该在/etc/os-release中:

~]# cat /etc/os-release
NAME="NeoKylin Linux Advanced Server"
VERSION="V7Update6 (Chromium)"
ID="neokylin"
ID_LIKE="fedora"
VARIANT="Server"
VARIANT_ID="server"
VERSION_ID="V7Update6"
PRETTY_NAME="NeoKylin Linux Advanced Server V7Update6 (Chromium)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:neokylin:enterprise_linux:V7Update6:GA:server"
HOME_URL="https://www.cs2c.com.cn/"
BUG_REPORT_URL="https://bugzilla.cs2c.com.cn/"

NEOKYLIN_BUGZILLA_PRODUCT="NeoKylin Linux Advanced Server 7"
NEOKYLIN_BUGZILLA_PRODUCT_VERSION=V7Update6
NEOKYLIN_SUPPORT_PRODUCT="NeoKylin Linux Advanced Server"
NEOKYLIN_SUPPORT_PRODUCT_VERSION="V7Update6"

这里果然可以看到VERSION_ID的值被定义为V7Update6,而系统原生发行版中该值是7,我们来看下os-release中对VERSION_ID参数的说明:

man os-release
... ...

       VERSION_ID=
           A lower-case string (mostly numeric, no spaces or other characters outside of 0-9, a-z, ".",
           "_" and "-") identifying the operating system version, excluding any OS name information or
           release code name, and suitable for processing by scripts or usage in generated filenames. This
           field is optional. Example: "VERSION_ID=17" or "VERSION_ID=11.04".
... ...

根据man文档中的描述,VERSION_ID取值范围为全小写,通常为数值型,不应有空格或其他特殊字符,可包含的字符为0-9a-z._-,那么这里可以看到两个问题, 第一个问题是kylin的VERSION_ID不符合此描述,包含了大写字符,第二个问题是VERSION_ID可以包含a-z字母,但是通常是数值如17,11.04等。 但由于常见发行版都将此处处理为数值型,就导致ansible按照此约定俗成固化了其获取系统版本的方法,并试图将一个字符串转换为int,不能满足当VERSION_ID包含了字母的情况。

验证结论

通过以上判断看到VERSION_ID是导致该问题现象的关键,那么我们可以尝试修改一下该参数值,再执行setup看看是否可以正常工作:

# grep VERSION_ID /etc/os-release
VERSION_ID="7"

这里我把VERSION_ID修改成了数字7,再执行setup观察ansible_pkg_mgr变量是否能获取到:

# ansible -i hosts node01 -m setup -a "filter=ansible_pkg_mgr"
node01 | SUCCESS => {
    "ansible_facts": {
        "ansible_pkg_mgr": "yum",
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false
}

可以看到,修改os-release中VERSION_ID为纯数值后,setup就可以正常判断到系统版本,进而可以获取到正确的yum版本了。 通过以上可以看到操作系统中即便是一些不起眼的细枝末节,处理不当也可能引发"连锁反应"。

· 阅读需 4 分钟

在使用bashrc和profile设置环境变量时,如果多个地方都有同一个变量的设置,则需要注意不同配置文件的加载顺序问题

背景

如果加载顺序没弄明白,有可能会在使用过程中遇到各种困扰,比如为什么设置了profile但是环境变量不生效?为什么变量ssh后获取的不一样?下面我们以CentOS7系统为例,通过一个简单的小实验来观察下到底bash的几个配置文件加载顺序是怎样的。

我们知道可以用来设置环境变量的文件常用的有以下几个:

  • /etc/profile
  • /etc/profile.d/*.sh
  • /etc/bashrc
  • ~/.bash_profile
  • ~/.bashrc

而不同的文件加载时机又分为login shell和non-login shell两种情况。这两种情况需要区分对待,及不同的文件要在对应场景下才能生效。假设有一个相同的变量设置出现在各个文件里面,通过对不同文件的变量值进行差异设置即可观察出各个配置的加载优先级和生效情况。

实验

先写入各个配置文件如下:

# tail -n1 /etc/profile /etc/bashrc /etc/profile.d/well.sh ~/.bash_profile ~/.bashrc
==> /etc/profile <==
export WELL=etc-profile

==> /etc/bashrc <==
export WELL=etc-bashrc

==> /etc/profile.d/well.sh <==
export WELL=etc-profile-d

==> /root/.bash_profile <==
export WELL=home-bash-profile

==> /root/.bashrc <==
export WELL=home-bashrc

接下来开始观察,需要注意的是每次修改配置之后新开shell重新加载环境配置:

[root@localhost ~]# echo $WELL
home-bash-profile
[root@localhost ~]# ssh localhost 'echo $WELL'
home-bashrc
[root@localhost ~]#


[root@localhost ~]# sed -i '$d' ~/.bashrc
[root@localhost ~]# sed -i '$d' ~/.bash_profile
[root@localhost ~]#


[root@localhost ~]# echo $WELL
etc-bashrc
[root@localhost ~]# ssh localhost 'echo $WELL'
etc-bashrc
[root@localhost ~]#


[root@localhost ~]# sed -i '$d' /etc/bashrc


[root@localhost ~]# echo $WELL
etc-profile
[root@localhost ~]# ssh localhost 'echo $WELL'
etc-profile-d
[root@localhost ~]#

# 重新写入~/.bashrc后
[root@localhost ~]# echo $WELL
home-bashrc
[root@localhost ~]# ssh localhost 'echo $WELL'
etc-profile-d
[root@localhost ~]#


# 重新写入~/.bash_profile,去掉~/.bashrc后
[root@localhost ~]# echo $WELL
home-bash-profile
[root@localhost ~]# ssh localhost 'echo $WELL'
etc-profile-d
[root@localhost ~]#

需要注意的是以上测试是将变量放到每个配置末行,因为配置之间有互相加载的机制,如果放在其他位置则测试结果会不一样。

结论

观察上面的结果,可以得出以下实验结论:

1 login shell会加载所有配置,优先级为~/.bash_profile ~/.bashrc /etc/bashrc /etc/profile /etc/profile.d

2 non-login shell时加载优先级为 ~/.bashrc /etc/bashrc /etc/profile.d

3 non-login shell不会加载的配置有 ~/.bash_profile /etc/profile

4 两种情况下都会加载的有~/.bashrc /etc/bashrc /etc/profile.d

那么如果我们需要在系统全局设置一个环境变量,要保证login shell和non-login shell都能表现一致,需要如何设置呢?

因为~/.bashrc为用户局部配置文件,不影响全局,而/etc/bashrc为系统内置文件不建议修改,如果是有全局环境变量需要设置建议放置到/etc/profile.d

over.

· 阅读需 4 分钟

一个项目的changelog对于使用者来说虽然不需要重点关注,但很重要

基本思路

通常软件产品对外发布时,我们需要提供一份changelog以告知使用者新版本所发生的变化,有两种方式可以产生需要的changelog内容, 一种是人工整理和编写,另外一种是通过工具实现自动化。这里介绍一种通过开源工具的组合快速实现自动生成的方法。

我们在开发过程中所有变更都会反映到git commit messages里面,git提交历史几乎可以反映软件的所有变更,基于此我们可以使用工具直接将git提交历史转化为changelog,再经过简单加工处理即可对外输出一个html页面。

规范提交

这就要求在代码提交过程中我们的commit message要规范化,其中一种被广为认可的规范名为约定式提交。详细可参考约定式提交 一个简单的提交类型参考如下:

  • build: 变更仅影响工具出包或者build环境等外部依赖问题
  • ci: 对CI配置的变更
  • docs: 仅文档内容变更
  • feat: 新特性
  • fix: bug修复
  • perf: 无bug修复/无新特性,仅性能提升
  • refactor: 无bug修复/无新特性/无性能提升,仅重构
  • style: 仅代码风格更改
  • test: 仅测试代码变更

提交转化为markdown

有了规范的提交记录,下面就可以通过工具实现提交记录到markdown的转化。这里介绍一个工具叫conventional-changelog,命令行版本使用方法如下:

# install
npm install -g conventional-changelog-cli
# generate changelog markdown file
cd your-git-repo-project-home
conventional-changelog -p angular -i CHANGELOG.md -s -r 0

示例中用到的参数:

  • -i : 读入已有changelog文件
  • -p : 预设模板,可以是angular/atom/codemirror/ember/eslint/express/jquery/jscs/jshint
  • -s : 写到目标文件名和-i指定的文件同名
  • -r : 指定需要生成的release数量,0表示重新生成所有

更多参数可以执行conventional-changelog --help查看。

markdown转化为html

这样我们就得到了一份名为CHANGELOG.md的历史变更记录文件,为markdown格式。接下来再通过另外一个工具名叫strapdown.js来自动生成html。

strapdown.js是一个js文件,不需要像上面生成markdown那样在server端生成,只需要在单个html页面中引入该js文件即可实现从markdown自动渲染出html页面。详细可参考strapdown.js

使用方法如下:

cat >changelog.html <<"EOF"
<!DOCTYPE html>
<html>
<title>XXX Changelog</title>
<meta charset="utf-8">
<xmp theme="darkly" style="display:none;">
EOF

cat CHANGELOG.md >>changelog.html
cat >>changelog.html <<"EOF"
</xmp>
<script src="http://strapdownjs.com/v/0.2/strapdown.js"></script>
</html>

这样我们就通过拼接的方式生成了一份changelog.html。需要注意的是changlog内容中不能包含</xmp>关键字。

over.

· 阅读需 2 分钟

某些特殊紧急情况下... ...

某些特殊紧急情况下没法等到重新从源码编译打包,手里只有一个打包好的rpm,但是里面内容需要在安装前就改掉,比如修改某个文件内容等,这个时候rpmrebuild命令可以派上用场。 rpmrebuild工作时会把rpm包内容释放到一个临时目录,如果需要修改rpm包里面的文件的话, 可以通过-m参数指定执行的命令,比如/bin/bash,这样就可以得到一个交互式的shell, 有了交互式shell想象空间就很大了,你可以在这个shell环境下对rpm包释放出来的文件任意修改,当退出这个shell时,rpmrebuild会把改动打包回新的rpm。 例如:

rpmrebuild -m /bin/bash -np rpm/xxx.rpm
# 此时我们得到一个交互shell,
# 比如知道需要修改的文件名为aaa,可以这样操作:
find / -name aaa
# 尽情发挥吧,完了退出
ctrl+D

现在你就得到修改好内容之后的新rpm了。

· 阅读需 1 分钟

编码问题很烦人

gbk编码的zip在linux下解压出来文件名会乱码,可以用下面脚本解压过程中转换下

#!/usr/bin/env python2
# coding: utf-8

import os
import sys
import zipfile

f = zipfile.ZipFile(sys.argv[1],"r");
for n in f.namelist():
    try:
        u = n.decode("gbk")
    except:
        u = n
    p = os.path.dirname(u)
    if not p:
        continue
    if not os.path.exists(p):
        os.makedirs(p)
    d = file.read(n)
    if os.path.exists(u):
        continue
    with open(u, "w") as o:
        o.write(data)

· 阅读需 2 分钟

电脑双系统centos+windows,安装完centos8之后默认没有引导windows的入口,按照下面方法手搓即可。

1 启动进入centos

查看磁盘分区信息,如下: fdisk -l

# fdisk -l
Disk /dev/sda: 238.5 GiB, 256060514304 bytes, 500118192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x297f5cef

Device     Boot     Start       End   Sectors   Size Id Type
/dev/sda1  *         2048 250058751 250056704 119.2G  7 HPFS/NTFS/exFAT
/dev/sda2       250058752 393418751 143360000  68.4G  7 HPFS/NTFS/exFAT
/dev/sda3       393418752 394442751   1024000   500M 83 Linux
/dev/sda4       394442752 500117503 105674752  50.4G  5 Extended
/dev/sda5       394444800 500117503 105672704  50.4G 83 Linux

通过fdisk结果看到windows第一个partion在sda1,对应grub的磁盘索引编号是hd0,1,接下来编辑grub配置文件,自定义配置路径:

 vi  /etc/grub.d/40_custom

配置示例如下:

 #!/bin/sh
 exec tail -n +3 $0
 # This file provides an easy way to add custom menu entries.  Simply type the
 # menu entries you want to add after this comment.  Be careful not to change
 # the 'exec tail' line above.

 menuentry "Windows" {
         set root=(hd0,1)
         chainloader +1
         }

保存并执行以下命令使自定义配置生效:

grub2-mkconfig --output=/boot/grub2/grub.cfg

OVER.

· 阅读需 31 分钟

这里是一句长长的引言

  • Shell 编码规范

前言

与其它的编程规范一样,这里所讨论的不仅仅是编码格式美不美观的问题, 同时也讨论一些约定及编码标准。这份文档主要侧重于我们所普遍遵循的规则, 对于那些不是明确强制要求的,我们尽量避免提供意见。

为什么要有编码规范

编码规范对于程序员而言尤为重要,有以下几个原因:

  • 一个软件的生命周期中,80%的花费在于维护
  • 几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护
  • 编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码
  • 如果你将源码作为产品发布,就需要确任它是否被很好的打包并且清晰无误,一如你已构建的其它任何产品

编码规范原则

本文档中的准则致力于最大限度达到以下原则:

  • 正确性
  • 可读性
  • 可维护性
  • 可调试性
  • 一致性
  • 美观

尽管本文档涵盖了许多基础知识,但应注意的是,没有编码规范可以为我们回答所有问题,开发人员始终需要再编写完代码后,对上述原则做出正确的判断。

代码规范等级定义

  • 可选(Optional):用户可参考,自行决定是否采用;
  • 推荐(Preferable):用户理应采用,但如有特殊情况,可以不采用;
  • 必须(Mandatory):用户必须采用(除非是少数非常特殊的情况,才能不采用);

注: 未明确指明的则默认为 必须(Mandatory)

本文档参考

主要参考如下文档:

源文件

基础

使用场景

仅建议Shell用作相对简单的实用工具或者包装脚本。因此单个shell脚本内容不宜太过复杂。

在选择何时使用shell脚本时时应遵循以下原则:

  • 如主要用于调用其他工具且需处理的数据量较少,则shell是一个选择
  • 如对性能十分敏感,则更推荐选择其他语言,而非shell
  • 如需处理相对复杂的数据结构,则更推荐选择其他语言,而非shell
  • 如脚本内容逐渐增长且有可能出现继续增长的趋势,请尽早使用其他语言重写

文件名

可执行文件不建议有扩展名,库文件必须使用 .sh 作为扩展名,且应是不可执行的。

执行一个程序时,无需知道其编写语言,且shell脚本并不要求具有扩展名,所以更倾向可执行文件没有扩展名。

而库文件知道其编写语言十分重要,使用 .sh 作为特定语言后缀的扩展名,可以和其他语言编写的库文件加以区分。

文件名要求全部小写, 可以包含下划线 _ 或连字符 -, 建议可执行文件使用连字符,库文件使用下划线。

正例:

my-useful-bin
my_useful_libraries.sh
myusefullibraries.sh

反例:

My_Useful_Bin
myUsefulLibraries.sh

文件编码

源文件编码格式为UTF-8。 避免不同操作系统对文件换行处理的方式不同,一律使用LF

单行长度

每行最多不超过120个字符。每行代码最大长度限制的根本原因是过长的行会导致阅读障碍,使得缩进失效。

除了以下两种情况例外:

  • 导入模块语句
  • 注释中包含的URL

如出现长度必须超过120个字符的字符串,应尽量使用here document或者嵌入的换行符等合适的方法使其变短。

示例:

# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END

# Embedded newlines are ok too
long_string="I am an exceptionally
  long string."

空白字符

除了在行结束使用换行符,空格是源文件中唯一允许出现的空白字符。

  • 字符串中的非空格空白字符,使用转义字符
  • 不允许行前使用tab缩进,如果使用tab缩进,必须设置1个tab为4个空格
  • 不应在行尾出现没有意义的空白字符

垃圾清理 推荐

对从来没有用到的或者被注释的方法、变量等要坚决从代码中清理出去,避免过多垃圾造成干扰。

结构

使用bash

Bash 是唯一被允许使用的可执行脚本shell。

可执行文件必须以 #!/bin/bash 开始。请使用 set 来设置shell的选项,使得用 bash <script_name> 调用你的脚本时不会破坏其功能。

限制所有的可执行shell脚本为bash使得我们安装在所有计算机中的shell语言保持一致性。 正例:

#!/bin/bash
set -e

反例:

#!/bin/sh -e

许可证或版权信息 推荐

许可证与版权信息需放在源文件的起始位置。例如:

#
# Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
#

缩进

块缩进

每当开始一个新的块,缩进增加4个空格(不能使用\t字符来缩进)。当块结束时,缩进返回先前的缩进级别。缩进级别适用于代码和注释。

main() {
    # 缩进4个空格
    say="hello"
    flag=0
    if [[ $flag = 0 ]]; then
        # 缩进4个空格
        echo "$say"
    fi
管道

如果一行容不下整个管道操作,那么请将整个管道操作分割成每行一个管段。

如果一行容得下整个管道操作,那么请将整个管道操作写在同一行,管道左右应有空格。

否则,应该将整个管道操作分割成每行一段,管道操作的下一部分应该将管道符放在新行并且缩进4个空格。这适用于管道符 | 以及逻辑运算 ||&& 。 正例:

# 单行管道连接,管道左右空格
command1 | command2

# 长命令管道换行连接,管道放置于下一个命令开头,缩进4个空格
command1 \
    | command2 \
    | command3 \
    | command4

反例:

# 管道左右无空格
command1|command2

# 换行连接管道放置于行末
command1 | \
    command2 | \
    command3 | \
    command4
循环

请将 ; do , ; thenwhile , for , if 放在同一行。

shell中的循环略有不同,但是我们遵循跟声明函数时的大括号相同的原则。即: ; do , ; then 应该和 while/for/if 放在同一行。 else 应该单独一行。 结束语句应该单独一行且跟开始语句缩进对齐。

正例:

for dir in ${dirs_to_cleanup}; do
    if [[ -d "${dir}/${BACKUP_SID}" ]]; then
        log_date "Cleaning up old files in ${dir}/${BACKUP_SID}"
        rm "${dir}/${BACKUP_SID}/"*
        if [[ "$?" -ne 0 ]]; then
            error_message
        fi
    else
        mkdir -p "${dir}/${BACKUP_SID}"
        if [[ "$?" -ne 0 ]]; then
            error_message
        fi
    fi
done

反例:

function getBatchName()
{
    batch_name="batch"
    if [[ "$input5"x == *$batch_name* ]]
    then
        batch_name=$input5
    else if [[ "$input6"x == *$batch_name* ]]
    then
        batch_name=$input6
    else if [[ "$input7"x == *$batch_name* ]]
    then
        batch_name=$input7
        fi
        fi
    fi
}
case语句

通过4个空格缩进可选项。 可选项中的多个命令应该被拆分成多行,模式表达式、操作和结束符 ;; 在不同的行。 匹配表达式比 case 和 esac 缩进一级。多行操作要再缩进一级。 模式表达式前面不应该出现左括号。避免使用 ;&;;& 符号。 示例:

case "${expression}" in
    a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
    absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
    *)
        error "Unexpected expression '${expression}'"
        ;;
esac

只要整个表达式可读,简单的单行命令可以跟模式和 ;; 写在同一行。当单行容不下操作时,请使用多行的写法。 单行示例:

verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
    case "${flag}" in
        a) aflag='true' ;;
        b) bflag='true' ;;
        f) files="${OPTARG}" ;;
        v) verbose='true' ;;
        *) error "Unexpected option ${flag}" ;;
    esac
done

函数位置

将文件中所有的函数统一放在常量下面。不要在函数之间隐藏可执行代码。

如果你有函数,请将他们统一放在文件头部。只有includes, set 声明和常量设置可能在函数声明之前完成。不要在函数之间隐藏可执行代码。如果那样做,会使得代码在调试时难以跟踪并出现意想不到的结果。

主函数main

对于包含至少了一个其他函数的足够长的脚本,建议定义一个名为 main 的函数。对于功能简单的短脚本, main函数是没有必要的。

为了方便查找程序的入口位置,将主程序放入一个名为 main 的函数中,作为最底部的函数。这使其和代码库的其余部分保持一致性,同时允许你定义更多变量为局部变量(如果主代码不是一个函数就不支持这种做法)。 文件中最后的非注释行应该是对 main 函数的调用:

main "$@"

注释

代码注释的基本原则:

  • 注释应能使代码更加明确
  • 避免注释部分的过度修饰
  • 保持注释部分简单、明确
  • 在编码以前就应开始写注释
  • 注释应说明设计思路而不是描述代码的行为

注释与其周围的代码在同一缩进级别,#号与注释文本间需保持一个空格以和注释代码进行区分。

文件头

每个文件的开头是其文件内容的描述。除版权声明外,每个文件必须包含一个顶层注释,对其功能进行简要概述。

例如:

#!/bin/bash
#
# Perform hot backups of databases.
功能注释

主体脚本中除简洁明了的函数外都必须带有注释。库文件中所有函数无论其长短和复杂性都必须带有注释。

这使得其他人通过阅读注释即可学会如何使用你的程序或库函数,而不需要阅读代码。

所有的函数注释应该包含:

  • 函数的描述
  • 全局变量的使用和修改
  • 使用的参数说明
  • 返回值,而不是上一条命令运行后默认的退出状态

例如:

#!/bin/bash
#
# Perform hot backups of databases.

export PATH='/usr/sbin/bin:/usr/bin:/usr/local/bin'

#######################################
# Cleanup files from the backup dir
# Globals:
#   BACKUP_DIR
#   BACKUP_SID
# Arguments:
#   None
# Returns:
#   None
#######################################
cleanup() {
    ...
}
实现部分的注释

注释你代码中含有技巧、不明显、有趣的或者重要的部分。

这部分遵循代码注释的基本原则即可。不要注释所有代码。如果有一个复杂的不易理解的逻辑,请进行简单的注释。

TODO注释

对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.

TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的名字,邮件地址, bug ID, 或其它身份标识和与这一 TODO 相关的 issue。 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的TODO 格式进行查找。 添加 TODO 注释并不意味着你要自己来修正,因此当你加上带有姓名的 TODO 时, 一般都是写上自己的名字。

这与C++ Style Guide中的约定相一致。

例如:

# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
# TODO(--bug=123456): remove the "Last visitors" feature

命名

函数名

使用小写字母,并用下划线分隔单词。使用双冒号 :: 分隔包名。函数名之后必须有圆括号。

如果你正在写单个函数,请用小写字母来命名,并用下划线分隔单词。如果你正在写一个包,使用双冒号 :: 来分隔包名。 函数名和圆括号之间没有空格,大括号必须和函数名位于同一行。 当函数名后存在 () 时,关键词 function 是多余的,建议不带 function 的写法,但至少做到同一项目内风格保持一致。 正例:

# Single function
my_func() {
  ...
}

# Part of a package
mypackage::my_func() {
  ...
}

反例:

function my_func
{
    ...
}

变量名

规则同函数名一致。

循环中的变量名应该和正在被循环的变量名保持相似的名称。 示例:

for zone in ${zones}; do
    something_with "${zone}"
done

常量和环境变量名

全部大写,用下划线分隔,声明在文件的顶部。

常量和任何导出到环境中的变量都应该大写。 示例:

# Constant
readonly PATH_TO_FILES='/some/path'

# Both constant and environment
declare -xr BACKUP_SID='PROD'

有些情况下首次初始化及常量(例如,通过getopts),因此,在getopts中或基于条件来设定常量是可以的,但之后应该立即设置其为只读。 值得注意的是,在函数中使用 declare 对全局变量无效,所以推荐使用 readonly 和 export 来代替。 示例:

VERBOSE='false'
while getopts 'v' flag; do
  case "${flag}" in
    v) VERBOSE='true' ;;
  esac
done
readonly VERBOSE

只读变量

使用 readonly 或者 declare -r 来确保变量只读。

因为全局变量在shell中广泛使用,所以在使用它们的过程中捕获错误是很重要的。当你声明了一个变量,希望其只读,那么请明确指出。 示例:

zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
  error_message
else
  readonly zip_version
fi

局部变量

每次只声明一个变量,不要使用组合声明,比如a=1 b=2;

使用 local 声明特定功能的变量。声明和赋值应该在不同行。

必须使用 local 来声明局部变量,以确保其只在函数内部和子函数中可见。这样可以避免污染全局名称空间以及避免无意中设置可能在函数外部具有重要意义的变量。

当使用命令替换进行赋值时,变量声明和赋值必须分开。因为内建的 local 不会从命令替换中传递退出码。 正例:

my_func2() {
    local name="$1"
    # 命令替换赋值,变量声明和赋值需放到不同行:
    local my_var
    my_var="$(my_func)" || return
    ...
}

反例:

my_func2() {
    # 禁止以下写法: $? 将获取到'local'指令的返回值, 而非 my_func
    local my_var="$(my_func)"
    [[ $? -eq 0 ]] || return

    ...
}

异常与日志

异常

使用shell返回值来返回异常,并根据不同的异常情况返回不同的值。

日志

所有的错误信息都应被导向到STDERR,这样将有利于出现问题时快速区分正常输出和异常输出。

建议使用与以下函数类似的方式来打印正常和异常输出:

err() {
    echo "[$(date +'%FT%T%z')]: $@" >&2
}

if ! do_something; then
    err "Unable to do_something"
    exit "${E_DID_NOTHING}"
fi

编程实践 持续分类并完善

变量扩展 推荐

通常情况下推荐为变量加上大括号如 "${var}" 而不是 "$var" ,但具体也要视情况而定。

以下按照优先顺序列出建议:

  • 与现有代码保持一致
  • 单字符变量在特定情况下才需要被括起来
  • 使用引号引用变量,参考下一节:变量引用

详细示例如下: 正例:

# 位置变量和特殊变量,可以不用大括号:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."

# 当位置变量大于等于10,则必须有大括号:
echo "many parameters: ${10}"

# 当出现歧义时,必须有大括号:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"

# 使用变量扩展赋值时,必须有大括号:
DEFAULT_MEM=${DEFUALT_MEM:-"-Xms2g -Xmx2g -XX:MaxDirectMemorySize=4g"}

# 其他常规变量的推荐处理方式:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
    echo "file=${f}"
done < <(ls -l /tmp)

反例:

# 无引号, 无大括号, 特殊变量,单字符变量
echo a=$avar "b=$bvar" "PID=${$}" "${1}"

# 无大括号产生歧义场景:以下会被解析为 "${1}0${2}0${3}0",
# 而非 "${10}${20}${30}
set -- a b c
echo "$10$20$30"

变量引用 推荐

变量引用通常情况下应遵循以下原则:

  • 默认情况下推荐使用引号引用包含变量、命令替换符、空格或shell元字符的字符串
  • 在有明确要求必须使用无引号扩展的情况下,可不用引号
  • 字符串为单词类型时才推荐用引号,而非命令选项或者路径名
  • 不要对整数使用引号
  • 特别注意 [[ 中模式匹配的引号规则
  • 在无特殊情况下,推荐使用 $@ 而非 $*

以下通过示例说明:

# '单引号' 表示禁用变量替换
# "双引号" 表示需要变量替换

# 示例1: 命令替换需使用双引号
flag="$(some_command and its args "$@" 'quoted separately')"

# 示例2:常规变量需使用双引号
echo "${flag}"

# 示例3:整数不使用引号
value=32
# 示例4:即便命令替换输出为整数,也需要使用引号
number="$(generate_number)"

# 示例5:单词可以使用引号,但不作强制要求
readonly USE_INTEGER='true'

# 示例6:输出特殊符号使用单引号或转义
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."

# 示例7:命令参数及路径不需要引号
grep -li Hugo /dev/null "$1"

# 示例8:常规变量用双引号,ccs可能为空的特殊情况可不用引号
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}

# 示例9:正则用单引号,$1可能为空的特殊情况可不用引号
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}

# 示例10:位置参数传递推荐带引号的"$@",所有参数作为单字符串传递用带引号的"$*"
# content of t.sh
func_t() {
    echo num: $#
    echo args: 1:$1 2:$2 3:$3
}

func_t "$@"
func_t "$*"
# 当执行 ./t.sh a b c 时输出如下:
num: 3
args: 1:a 2:b 3:c
num: 1
args: 1:a b c 2: 3:

命令替换

使用 $(command) 而不是反引号。

因反引号如果要嵌套则要求用反斜杠转义内部的反引号。而 $(command) 形式的嵌套无需转义,且可读性更高。

正例:

var="$(command "$(command1)")"

反例:

var="`command \`command1\``"

条件测试

使用 [[ ... ]] ,而不是 [ , test , 和 /usr/bin/[

因为在 [[]] 之间不会出现路径扩展或单词切分,所以使用 [[ ... ]] 能够减少犯错。且 [[ ... ]] 支持正则表达式匹配,而 [ ... ] 不支持。 参考以下示例:

# 示例1:正则匹配,注意右侧没有引号
# 详尽细节参考:http://tiswww.case.edu/php/chet/bash/FAQ 中E14部分
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
    echo "Match"
fi

# 示例2:严格匹配字符串"f*"(本例为不匹配)
if [[ "filename" == "f*" ]]; then
    echo "Match"
fi

# 示例3:[]中右侧不加引号将出现路径扩展,如果当前目录下有f开头的多个文件将报错[: too many arguments
if [ "filename" == f* ]; then
    echo "Match"
fi

字符串测试

尽可能使用变量引用,而非字符串过滤。

Bash可以很好的处理空字符串测试,请使用空/非空字符串测试方法,而不是过滤字符,让代码具有更高的可读性。 正例:

if [[ "${my_var}" = "some_string" ]]; then
    do_something
fi

反例:

if [[ "${my_var}X" = "some_stringX" ]]; then
    do_something
fi

正例:

# 使用-z测试字符串为空
if [[ -z "${my_var}" ]]; then
    do_something
fi

反例:

# 使用空引号测试空字符串,能用但不推荐
if [[ "${my_var}" = "" ]]; then
    do_something
fi

正例:

# 使用-n测试非空字符串
if [[ -n "${my_var}" ]]; then
    do_something
fi

反例:

# 测试字符串非空,能用但不推荐
if [[ "${my_var}" ]]; then
    do_something
fi

文件名扩展

当进行文件名的通配符扩展时,请指定明确的路径。

当目录中有特殊文件名如以 - 开头的文件时,使用带路径的扩展通配符 ./* 比不带路径的 * 要安全很多。

# 例如目录下有以下4个文件和子目录:
# -f  -r  somedir  somefile

# 未指定路径的通配符扩展会把-r和-f当作rm的参数,强制删除文件:
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'

# 而指定了路径的则不会:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'

慎用eval

应该避免使用eval。

Eval在用于分配变量时会修改输入内容,但设置变量的同时并不能检查这些变量是什么。 反例:

# 以下设置的内容及成功与否并不明确
eval $(set_my_variables)

慎用管道连接while循环

请使用进程替换或者for循环,而不是通过管道连接while循环。

这是因为在管道之后的while循环中,命令是在一个子shell中运行的,因此对变量的修改是不能传递给父shell的。

这种管道连接while循环中的隐式子shell使得bug定位非常困难。 反例:

last_line='NULL'
your_command | while read line; do
    last_line="${line}"
done

# 以下会输出'NULL':
echo "${last_line}"

如果你确定输入中不包含空格或者其他特殊符号(通常不是来自用户输入),则可以用for循环代替。 例如:

total=0
# 仅当返回结果中无空格等特殊符号时以下可正常执行:
for value in $(command); do
    total+="${value}"
done

使用进程替换可实现重定向输出,但是请将命令放入显式子shell,而非while循环创建的隐式子shell。 例如:

total=0
last_file=
# 注意两个<之间有空格,第一个为重定向,第二个<()为进程替换
while read count filename; do
    total+="${count}"
    last_file="${filename}"
done < <(your_command | uniq -c)

echo "Total = ${total}"
echo "Last one = ${last_file}"

检查返回值

总是检查返回值,且提供有用的返回值。

对于非管道命令,使用 $? 或直接通过 if 语句来检查以保持其简洁。

例如:

# 使用if语句判断执行结果
if ! mv "${file_list}" "${dest_dir}/" ; then
    echo "Unable to move ${file_list} to ${dest_dir}" >&2
    exit "${E_BAD_MOVE}"
fi

# 或者使用$?
mv "${file_list}" "${dest_dir}/"
if [[ $? -ne 0 ]]; then
    echo "Unable to move ${file_list} to ${dest_dir}" >&2
    exit "${E_BAD_MOVE}"
fi

内建命令和外部命令

当内建命令可以完成相同的任务时,在shell内建命令和调用外部命令之间,应尽量选择内建命令。

因内建命令相比外部命令而言会产生更少的依赖,且多数情况调用内建命令比调用外部命令可以获得更好的性能(通常外部命令会产生额外的进程开销)。

正例:

# 使用内建的算术扩展
addition=$((${X} + ${Y}))
# 使用内建的字符串替换
substitution="${string/#foo/bar}"

反例:

# 调用外部命令进行简单的计算
addition="$(expr ${X} + ${Y})"
# 调用外部命令进行简单的字符串替换
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"

文件加载

加载外部库文件不建议用使用.,建议使用source,已提升可阅读性。 正例:

source my_libs.sh

反例:

. my_libs.sh

内容过滤与统计

除非必要情况,尽量使用单个命令及其参数组合来完成一项任务,而非多个命令加上管道的不必要组合。 常见的不建议的用法例如:cat和grep连用过滤字符串; cat和wc连用统计行数; grep和wc连用统计行数等。

正例:

grep net.ipv4 /etc/sysctl.conf
grep -c net.ipv4 /etc/sysctl.conf
wc -l /etc/sysctl.conf

反例:

cat /etc/sysctl.conf | grep net.ipv4
grep net.ipv4 /etc/sysctl.conf | wc -l
cat /etc/sysctl.conf | wc -l

正确使用返回与退出

除特殊情况外,几乎所有函数都不应该使用exit直接退出脚本,而应该使用return进行返回,以便后续逻辑中可以对错误进行处理。 正例:

# 当函数返回后可以继续执行cleanup
my_func() {
    [[ -e /dummy ]] || return 1
}

cleanup() {
    ...
}

my_func
cleanup

反例:

# 当函数退出时,cleanup将不会被执行
my_func() {
    [[ -e /dummy ]] || exit 1
}

cleanup() {
    ...
}

my_func
cleanup

附:常用工具

推荐以下工具帮助我们进行代码的规范:

· 阅读需 1 分钟

介绍hive小文件常见处理方法

hive的文件产生过程

小文件太多的影响

为什么会产生小文件

如何处理小文件

case 1

INSERT OVERWRITE TABLE tb1
    SELECT * FROM tb2
ORDER BY 1;
ALTER TABLE tb2 RENAME TO b_tb2;
ALTER TABLE tb1 RENAME TO tb2;

case 2

INSERT TABLE tb1
SELECT c1, c2 FROM (
    SELECT c1, c2
    FROM tb2
    WHERE xxx
      AND xxx
) t
ORDER BY c1, c2;

case 3

SELECT c1
FROM  (
    xxx
) t
GROUP BY x;

case 4

INSERT OVERWRITE TABLE tb1
SELECT
    xxx
FROM
    xxx
    WHERE
        xxx) t
distribute by rand();

case 5

INSERT TABLE tb1
SELECT c1,c2
FROM tb2
WHERE xxx
sort by c1;

· 阅读需 1 分钟

本文试着总结系统中的随机数前前后后以及管理中需要注意的问题 [先欠着]

1 什么是随机数?

随机数就是无法预测的数

2 随机数有什么用?

随机是为了安全

3 如何获得随机数?

/dev/random /dev/urandom /proc/sys/kernel/random/entropy_avail

4 会有哪些问题?

1 随机数数产生速度慢 2 影响上层应用

haveged 和 rng-tools