当前位置: 首页 > news >正文

linux shell 入门学习笔记2shell脚本

什么是shell脚本

当命令或者程序语句写在文件中,我们执行文件,读取其中的代码,这个程序就称之为shell脚本。
有了shell脚本肯定是要有对应的解释器了,常见的shell脚本解释器有sh、python、perl、tcl、php、ruby等。一般这种使用文件方式来执行sh命令的方式被称为非交互方式。

  • Windows中存在的*.bat批处理脚本。
  • Linux中常用*.sh脚本文件。

shell脚本规则

在Linux系统中,shell脚本或者称之为bash shell程序,通常都是vim编辑,有Linux命令、bash shell指令、逻辑控制语句和注释信息组成。
google bash编程规范链接

  • 1.使用.sh这样特定语言后缀作为扩展名,可以快速识别文件。
  • 2.set可以设置shell的选项。SUID(Set User ID)和SGID(Set Group ID)在shell脚本中是被禁止的,如果你需要较高权限的访问请使用sudo
  • 3.建议使用STDERR打印错误信息。
err() {echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
}if ! do_something; thenerr "Unable to do_something"exit "${E_DID_NOTHING}"
fi
  • 4.文件头:指定脚本解释器,这里是用#!/bin/bash做解释器的。
#!/bin/bash
#
# Perform hot backups of Oracle databases.
  • 5.函数注释应该包括:
    • 函数的描述
    • 全局变量的使用和修改
    • 使用的参数说明
    • 返回值,而不是上一条命令运行后默认的退出状态
      例如:
#!/bin/bash
#
# Perform hot backups of Oracle databases.export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin'#######################################
# Cleanup files from the backup dir
# Globals:
#   BACKUP_DIR
#   ORACLE_SID
# Arguments:
#   None
# Returns:
#   None
#######################################
cleanup() {...
}
  • 6.TODO注释应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上 bug或者ticket 的序号。
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
  • 7.缩进一般使用两个空格,没有制表符。
  • 8.行的长度和长字符串最大长度为80个字符。
    使用下述两种方式规避长字符串:
# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END# Embedded newlines are ok too
long_string="I am an exceptionallylong string."
  • 9.管道尽量写在一行,否则应该将整个管道操作分割成每行一个管段,管道操作的下一部分应该将管道符放在新行并且缩进2个空格。这适用于使用管道符’|’的合并命令链以及使用’||’和’&&’的逻辑运算链。
# All fits on one line
command1 | command2# Long commands
command1 \| command2 \| command3 \| command4
  • 10.循环中,; do , ; then 应该和 if/for/while 放在同一行。 else 应该单独一行,结束语句应该单独一行并且跟开始语句垂直对齐。
    例如:
for dir in ${dirs_to_cleanup}; doif [[ -d "${dir}/${ORACLE_SID}" ]]; thenlog_date "Cleaning up old files in ${dir}/${ORACLE_SID}"rm "${dir}/${ORACLE_SID}/"*if [[ "$?" -ne 0 ]]; thenerror_messagefielsemkdir -p "${dir}/${ORACLE_SID}"if [[ "$?" -ne 0 ]]; thenerror_messagefifi
done
  • 11.case语句模式表达式前面不应该出现左括号。避免使用 ;&;;& 符号。关键规则如下:
    * 通过2个空格缩进可选项。
    * 在同一行可选项的模式右圆括号之后和结束符 ;; 之前各需要一个空格。
    * 长可选项或者多命令可选项应该被拆分成多行,模式、操作和结束符 ;; 在不同的行。
    例子1:
case "${expression}" ina)variable="..."some_command "${variable}" "${other_expr}" ...;;absolute)actions="relative"another_command "${actions}" "${other_expr}" ...;;*)error "Unexpected expression '${expression}'";;
esac

例子2:

verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; docase "${flag}" ina) aflag='true' ;;b) bflag='true' ;;f) files="${OPTARG}" ;;v) verbose='true' ;;*) error "Unexpected option ${flag}" ;;esac
done
  • 12.变量扩展应该保持跟你所发现的一致,引用你的变量,推荐用 ${var} 而不是 $var
    例如:
# Section of recommended cases.# Preferred style for 'special' variables:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."# Braces necessary:
echo "many parameters: ${10}"# Braces avoiding confusion:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"# Preferred style for other variables:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; doecho "file=${f}"
done < <(ls -l /tmp)# Section of discouraged cases# Unquoted vars, unbraced vars, brace-quoted single letter
# shell specials.
echo a=$avar "b=$bvar" "PID=${$}" "${1}"# Confusing use: this is expanded as "${1}0${2}0${3}0",
# not "${10}${20}${30}
set -- a b c
echo "$10$20$30"
  • 13.引用
    • 除非需要小心不带引用的扩展,否则总是引用包含变量、命令替换符、空格或shell元字符的字符串
    • 推荐引用是单词的字符串(而不是命令选项或者路径名)
    • 千万不要引用整数
    • 注意 [[ 中模式匹配的引用规则
    • 请使用 $@ 除非你有特殊原因需要使用 $*
      例如:
# 'Single' quotes indicate that no substitution is desired.
# "Double" quotes indicate that substitution is required/tolerated.# Simple examples
# "quote command substitutions"
flag="$(some_command and its args "$@" 'quoted separately')"# "quote variables"
echo "${flag}"# "never quote literal integers"
value=32
# "quote command substitutions", even when you expect integers
number="$(generate_number)"# "prefer quoting words", not compulsory
readonly USE_INTEGER='true'# "quote shell meta characters"
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."# "command options or path names"
# ($1 is assumed to contain a value here)
grep -li Hugo /dev/null "$1"# Less simple examples
# "quote variables, unless proven false": ccs might be empty
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}# Positional parameter precautions: $1 might be unset
# Single quotes leave regex as-is.
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}# For passing on arguments,
# "$@" is right almost everytime, and
# $* is wrong almost everytime:
#
# * $* and $@ will split on spaces, clobbering up arguments
#   that contain spaces and dropping empty strings;
# * "$@" will retain arguments as-is, so no args
#   provided will result in no args being passed on;
#   This is in most cases what you want to use for passing
#   on arguments.
# * "$*" expands to one argument, with all args joined
#   by (usually) spaces,
#   so no args provided will result in one empty string
#   being passed on.
# (Consult 'man bash' for the nit-grits ;-)set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@")
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@")
  • 14.命令替换应使用$(command) 而不是反引号
# This is preferred:
var="$(command "$(command1)")"# This is not:
var="`command \`command1\``"
  • 15.test, [和[[时,推荐使用 [[ ... ]] ,而不是 [ , test , 和 /usr/bin/[
# This ensures the string on the left is made up of characters in the
# alnum character class followed by the string name.
# Note that the RHS should not be quoted here.
# For the gory details, see
# E14 at http://tiswww.case.edu/php/chet/bash/FAQ
if [[ "filename" =~ [^[:digit:]]+name ]]; thenecho "Match"
fi# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; thenecho "Match"
fi# This gives a "too many arguments" error as f* is expanded to the
# contents of the current directory
if [ "filename" == f* ]; thenecho "Match"
fi
  • 16.测试字符串尽可能使用引用,而不是过滤字符串
    例子1
# Do this:
if [[ "${my_var}" = "some_string" ]]; thendo_something
fi# -z (string length is zero) and -n (string length is not zero) are
# preferred over testing for an empty string
if [[ -z "${my_var}" ]]; thendo_something
fi# This is OK (ensure quotes on the empty side), but not preferred:
if [[ "${my_var}" = "" ]]; thendo_something
fi# Not this:
if [[ "${my_var}X" = "some_stringX" ]]; thendo_something
fi

例子2

# Use this
if [[ -n "${my_var}" ]]; thendo_something
fi# Instead of this as errors can occur if ${my_var} expands to a test
# flag
if [[ "${my_var}" ]]; thendo_something
fi
  • 17.文件名的通配符扩展,当进行文件名的通配符扩展时,请使用明确的路径
# Here's the contents of the directory:
# -f  -r  somedir  somefile# This deletes almost everything in the directory by force
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'# As opposed to:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'
  • 18.Eval应该避免使用
    eval简单例子
# What does this set?
# Did it succeed? In part or whole?
eval $(set_my_variables)# What happens if one of the returned values has a space in it?
variable="$(eval some_function)"
  • 19.不建议管道导向while循环
  • 20.函数名,使用小写字母,并用下划线分隔单词。使用双冒号 :: 分隔库。函数名之后必须有圆括号。关键词 function 是可选的,但必须在一个项目中保持一致
# Single function
my_func() {...
}# Part of a package
mypackage::my_func() {...
}

当函数名后存在 () 时,关键词 function 是多余的。但是其促进了函数的快速辨识。

  • 21.变量名,如函数名命名相同,循环的变量名应该和循环的任何变量同样命名
for zone in ${zones}; dosomething_with "${zone}"
done
  • 22.常量和环境变量名,全部大写,用下划线分隔,声明在文件的顶部
# Constant
readonly PATH_TO_FILES='/some/path'# Both constant and environment
declare -xr ORACLE_SID='PROD'

动态生成的常量,可以使用readonlyexport来进行设置,在函数中declare不会对全局变量进行操作。

VERBOSE='false'
while getopts 'v' flag; docase "${flag}" inv) VERBOSE='true' ;;esac
done
readonly VERBOSE
  • 23.源文件名使用小写,如果需要的话使用下划线分隔单词
  • 24.只读变量使用readonly 或者 declare -r 来确保变量只读
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; thenerror_message
elsereadonly zip_version
fi
  • 25.本地变量使用 local 声明特定功能的变量。声明和赋值应该在不同行。local作用域为函数内部和子函数中可见。
my_func2() {local name="$1"# Separate lines for declaration and assignment:local my_varmy_var="$(my_func)" || return# DO NOT do this: $? contains the exit code of 'local', not my_funclocal my_var="$(my_func)"[[ $? -eq 0 ]] || return...
}
  • 26.函数位置:将文件中所有的函数一起放在常量下面。不要在函数之间隐藏可执行代码
  • 27.主函数main:为了方便查找程序的开始,将主程序放入一个称为 main 的函数,作为最下面的函数
  • 28.检查返回值:使用 $? 或直接通过一个 if 语句来检查以保持其简洁
    例子1
if ! mv "${file_list}" "${dest_dir}/" ; thenecho "Unable to move ${file_list} to ${dest_dir}" >&2exit "${E_BAD_MOVE}"
fi# Or
mv "${file_list}" "${dest_dir}/"
if [[ "$?" -ne 0 ]]; thenecho "Unable to move ${file_list} to ${dest_dir}" >&2exit "${E_BAD_MOVE}"
fi

例子2(Bash内部PIPESTATUS变量)
-作用

tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; thenecho "Unable to tar files to ${dir}" >&2
fi

例子3(PIPESTATUS优化)

tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
return_codes=(${PIPESTATUS[*]})
if [[ "${return_codes[0]}" -ne 0 ]]; thendo_something
fi
if [[ "${return_codes[1]}" -ne 0 ]]; thendo_something_else
fi
  • 29.内建命令和外部命令选择上请使用内建命令
# Prefer this:
addition=$((${X} + ${Y}))
substitution="${string/#foo/bar}"# Instead of this:
addition="$(expr ${X} + ${Y})"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"

相关文章:

linux shell 入门学习笔记2shell脚本

什么是shell脚本 当命令或者程序语句写在文件中&#xff0c;我们执行文件&#xff0c;读取其中的代码&#xff0c;这个程序就称之为shell脚本。 有了shell脚本肯定是要有对应的解释器了&#xff0c;常见的shell脚本解释器有sh、python、perl、tcl、php、ruby等。一般这种使用文…...

Android稳定性系列-01-使用 Address Sanitizer检测原生代码中的内存错误

前言想必大家曾经被各种Native Crash折磨过&#xff0c;本地测试没啥问题&#xff0c;一到线上或者自动化测试就出现各种SIGSEGV、SIGABRT、SIGILL、SIGBUS、SIGFPE异常&#xff0c;而且堆栈还是崩溃到libc.so这种&#xff0c;看起来跟我们的代码没啥关系&#xff0c;关键还不好…...

HyperOpt-quniform 范围问题

在使用 quniform 的时候&#xff0c;可能会出现超出指定范围的值&#xff0c;例如对于 GBDT 设置参数空间为 learning_rate:hp.quniform(learning_rate,0.05,2.05,0.2)&#xff0c;但是仍然会报错 ValueError: learning_rate must be greater than 0 but was 0.0&#xff0c;但…...

Pycharm搭建一个Django项目

File->new project 点击create&#xff0c; 等待一下即可 查看安装 Django 版本&#xff1a; 在 Pycharm 底部选择 Terminal 然后在里面输入&#xff1a;python -m django --version 启动项目&#xff1a; 在 Terminal 里面输入: python manage.py runserver 查看文件目…...

浅析前端工程化中的一部曲——模块化

在日益复杂和多元的 Web 业务背景下&#xff0c;前端工程化经常会被提及。工程化的目的是高性能、稳定性、可用性、可维护性、高效协同&#xff0c;只要是以这几个角度为目标所做的操作&#xff0c;都可成为工程化的一部分。工程化是软件工程中的一种思想&#xff0c;当下的工程…...

新版bing(集成ChatGPT)申请通过后在谷歌浏览器(Chrome)上的使用方法

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,科大讯飞比赛第三名,CCF比赛第四名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…...

Time-distributed 的理解

前言 今天看到论文中用到 Time-distributed CNN&#xff0c;第一次见到 Time-distributed&#xff0c;不理解是什么含义&#xff0c;看到代码实现也很懵。不管什么网络结构&#xff0c;外面都能套一个TimeDistributed。看了几个博客&#xff0c;还是不明白&#xff0c;问了问C…...

matlab 计算矩阵的Moore-Penrose 伪逆

目录 一、Moore-Penrose 伪逆1、主要函数2、输入输出参数二、代码示例使用伪逆求解线性方程组一、Moore-Penrose 伪逆 Moore-Penrose 伪逆是一种矩阵,可在不存在逆矩阵的情况下作为逆矩阵的部分替代。此矩阵常被用于求解没有唯一解或有许多解的线性方程组。    对于任何矩阵…...

简历制作方面的经验与建议

专栏推荐:2023 数字IC设计秋招复盘——数十家公司笔试题、面试实录 专栏首页:2023 数字IC设计秋招复盘——数十家公司笔试题、面试实录 专栏内容: 笔试复盘篇 2023秋招过程中整理的笔试题,来源包括我自己求职笔试以及整理其他同学的笔试。包含华为、中兴、联发科、AMD、大…...

C语言--static、const、volatile关键字

Static static修饰局部变量改变了变量的生命周期&#xff0c;让静态局部变量出了作用域依然存在&#xff0c;到程序结束&#xff0c;生命周期才结束。 static 修饰局部变量 改变局部变量的生命周期&#xff0c;本质上是改变了局部变量的存储位置&#xff0c;让局部变量不再是…...

Rust学习入门--【18】Rust结构体

系列文章目录 Rust 语言是一种高效、可靠的通用高级语言&#xff0c;效率可以媲美 C / C 。本系列文件记录博主自学Rust的过程。欢迎大家一同学习。 Rust学习入门–【1】引言 Rust学习入门–【2】Rust 开发环境配置 Rust学习入门–【3】Cargo介绍 Rust学习入门–【4】Rust 输…...

LeetCode142 环形链表Ⅱ

题目&#xff1a; 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评…...

JavaScript刷LeetCode拿offer-高频链表题

首先需要了解链表的概念 先把 next 记录下来 无论是插入&#xff0c;删除&#xff0c;还是翻转等等操作&#xff0c;先把 next 指针用临时变量保存起来&#xff0c;这可以解决 90% 重组链表中指向出错的问题&#xff0c; 如果不知道什么时候需要用到守卫&#xff0c;那就都用…...

linux系统编程2--网络编程

在linux系统编程中网络编程是使用socket&#xff08;套接字&#xff09;&#xff0c;socket这个词可以表示很多概念&#xff1a;在TCP/IP协议中&#xff0c;“IP地址TCP或UDP端口号”唯一标识网络通讯中的一个进程&#xff0c;“IP地址端口号”就称为socket。在TCP协议中&#…...

Allegro如何重命名光绘操作指导

Allegro如何重命名光绘操作指导 在做PCB设计的时候,光绘设置是输出生产文件必要的流程,设置好光绘之后,如何对光绘重新命名,如下图 如何把L1改成TOP,L6改成BOTTOM,具体操作步骤如下 点击Manufacture选择Artwork...

[PMLR 2018] Hyperbolic entailment cones for learning hierarchical embeddings

Contents IntroductionEntailment Cones in the Poincar BallConvex cones in a complete Riemannian manifoldAngular cones in the Poincar ballfour intuitive propertiesClosed form expression of the optimal ψ \psi...

2023春季露营投影怎么选?轻薄投影极米Z6X Pro值得推荐

近年来&#xff0c;露营经济在多重因素的共同助推下快速发展&#xff0c;精致露营的攻略开始占据小红书、微博、朋友圈等各类社交平台&#xff0c;吸引着更多用户种草并加入到露营大军中&#xff0c;而露营经济的强势“破圈”给家用智能投影带来了更多的发展契机。凭借着小巧的…...

收藏,核心期刊的投稿、审稿、出刊流程详解

学术期刊论文&#xff08;核心和普刊&#xff09;的发表流程总的来说其实是一样的&#xff0c;整个流程包括&#xff1a;1写作-2选择刊物-3投稿-4审稿-5返修或拒稿-6录用-7出刊-8上网检索。 其中1和2其实顺序是可以调换的&#xff0c;可以选择好刊物再写作&#xff0c;根据刊物…...

JVM类加载子系统

1、类加载子系统在内存结构中所处的位置通过内存结构图&#xff0c;我们先知道类加载子系统所处的位置&#xff0c;做到心中有图。2、类加载器作用类加载器子系统负责从文件系统或者网络中加载Class文件&#xff0c;class文件在文件开头有特定的文件标识。ClassLoader只负责cla…...

摄像头的镜头的几个知识点

1、镜头的组成及镜片的固定方式 摄像头的镜头结构主要分为镜身&#xff0c;透镜&#xff0c;变焦环&#xff0c;对焦环&#xff0c;光圈叶片&#xff0c;部分还有防抖系统&#xff0e;其中最重要的就是透镜&#xff0c;也叫镜片。镜片的主要原料是光学玻璃&#xff0c;玻璃&…...

分布式-分布式存储笔记

读写分离 什么时候需要读写分离 互联网大部分业务场景都是读多写少的&#xff0c;读和写的请求对比可能差了不止一个数量级。为了不让数据库的读成为业务瓶颈&#xff0c;同时也为了保证写库的成功率&#xff0c;一般会采用读写分离的技术来保证。 读写分离的实现是把访问的压…...

第十三届蓝桥杯国赛 C++ C 组 Java A 组 C 组 Python C 组 E 题——斐波那契数组(三语言代码AC)

目录1.斐波那契数组1.题目描述2.输入格式3.输出格式4.样例输入5.样例输出6.数据范围7.原题链接2.解题思路3.Ac_code1.Java2.C3.Python1.斐波那契数组 1.题目描述 如果数组 A(a0,a1,⋯.an−1)A(a_0,a_1,⋯.a_{n-1})A(a0​,a1​,⋯.an−1​)满足以下条件, 就说它是一个斐波那契…...

多因子模型(MFM)

多因子模型&#xff08;Muiti-Factor M: MFM&#xff09;因子投资基础CAPM (资本资产定价模型)APT套利定价理论截面数据 & 时间序列数据 & 面板数据定价误差 α\alphaαalpha 出现的原因线性多因子模型Fama-French三因子模型三因子的计算公式利用alpha大小进行购买股票…...

django项目实战一(django+bootstrap实现增删改查)

目录 一、创建django项目 二、修改默认配置 三、配置数据库连接 四、创建表结构 五、在app当中创建静态文件 六、页面实战-部门管理 1、实现一个部门列表页面 2、实现新增部门页面 3、实现删除部门 4、实现部门编辑功能 七、模版的继承 1、创建模板layout.html 1&…...

graphsage解读

传统的图方法都是直推式(transductive)的&#xff0c;学习到的是结构固定的图模型&#xff0c;一旦有新的节点加入&#xff0c;便需要重新训练整个图网络&#xff0c;泛化性不强。GraphSAGE是归纳式(inductive)的&#xff0c;它学习一种映射&#xff1a;通过采样和聚合邻居节点…...

一文带你读懂Dockerfile

目录 一、概述 二、DockerFile构建过程解析 &#xff08;一&#xff09;Dockerfile内容基础知识 &#xff08;二&#xff09;Docker执行Dockerfile的大致流程 &#xff08;三&#xff09;总结 三、DockerFile常用保留字指令 四、案例 &#xff08;一&#xff09;自定义…...

用python实现对AES加密的视频数据流解密

密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法。 在做网络爬虫的时候,会遇到经过AES加密的数据,可以使用python来进行解密。 在做爬虫的时候,通常可以找到一个key,这个key是一个十六进制的一串字符,这传字符是解密的关键。所以对于…...

网络高可用方案

目录 1. 网络高可用 2. 高可用方案设计 2.1 方案一 堆叠 ha负载均衡模式 2.2 方案二 OSPF ha负载均衡模式 3. 高可用保障 1. 网络高可用 网络高可用&#xff0c;是指对于网络的核心部分或设备在设计上考虑冗余和备份&#xff0c;减少单点故障对整个网络的影响。其设计应…...

简单的认识 Vue(vue-cli安装、node安装、开发者工具)

Vue1、Vue 与其他框架的对比及特点1.1 Vue.js 是什么1.2 作者1.3 作用1.4 Vue 与其他框架的对比2、安装 Vue 的方法2.1 CDN 引入2.2 脚手架工具2.3 vue 开发者工具安装3、创建第一个实例4、理解 Vue 的 MVVM 模式5、数据双向绑定5.1 感受响应式实验总结1、Vue 与其他框架的对比…...

如何写一个 things3 client

Things3[1] 是一款苹果生态内的任务管理软件&#xff0c;是一家德国公司做的&#xff0c;非常好用。我前后尝试了众多任务管理软件&#xff0c;最终选定 things3&#xff0c;以后有机会会写文章介绍我是如何用 things3 来管理我的日常任务。本文主要介绍欧神写的 tli[2] 工具来…...