Go 并发编程中的经验教训

Summary: 通过学习如何定位并发处理的陷阱来避免未来处理这些问题时的困境。

通过学习如何定位并发处理的陷阱来避免未来处理这些问题时的困境。

>
> 通过学习如何定位并发处理的陷阱来避免未来处理这些问题时的困境。
>
>
>

![](/data/attachment/album/202001/13/150539n217ak1vcf717uzx.jpg)

在复杂的分布式系统进行任务处理时，你通常会需要进行并发的操作。在 [Mode.net](http://mode.net) 公司，我们每天都要和实时、快速和灵活的软件打交道。而没有一个高度并发的系统，就不可能构建一个毫秒级的动态地路由数据包的全球专用网络。这个动态路由是基于网络状态的，尽管这个过程需要考虑众多因素，但我们的重点是链路指标。在我们的环境中，链路指标可以是任何跟网络链接的状态和当前属性（如链接延迟）有关的任何内容。

### 并发探测链接监控

我们的动态路由算法 [H.A.L.O.](https://people.ece.cornell.edu/atang/pub/15/HALO_ToN.pdf)（<ruby> 逐跳自适应链路状态最佳路由 <rt>  Hop-by-Hop Adaptive Link-State Optimal Routing </rt></ruby>）部分依赖于链路指标来计算路由表。这些指标由位于每个 PoP（<ruby> 存活节点 <rt>  Point of Presence </rt></ruby>）上的独立组件收集。PoP 是表示我们的网络中单个路由实体的机器，通过链路连接并分布在我们的网络拓扑中的各个位置。某个组件使用网络数据包探测周围的机器，周围的机器回复数据包给前者。从接收到的探测包中可以获得链路延迟。由于每个 PoP 都有不止一个临近节点，所以这种探测任务实质上是并发的：我们需要实时测量每个临近连接点的延迟。我们不能串行地处理；为了计算这个指标，必须尽快处理每个探测。

![latency computation graph](/data/attachment/album/202001/13/151444uzg0en5bhbet0hyb.png "latency computation graph")

### 序列号和重置：一个重新排列场景

我们的探测组件互相发送和接收数据包，并依靠序列号进行数据包处理。这旨在避免处理重复的包或顺序被打乱的包。我们的第一个实现依靠特殊的序列号 0 来重置序列号。这个数字仅在组件初始化时使用。主要的问题是我们考虑了递增的序列号总是从 0 开始。在该组件重启后，包的顺序可能会重新排列，某个包的序列号可能会轻易地被替换成重置之前使用过的值。这意味着，后继的包都会被忽略掉，直到排到重置之前用到的序列值。

### UDP 握手和有限状态机

这里的问题是该组件重启前后的序列号是否一致。有几种方法可以解决这个问题，经过讨论，我们选择了实现一个带有清晰状态定义的三步握手协议。这个握手过程在初始化时通过链接建立会话。这样可以确保节点通过同一个会话进行通信且使用了适当的序列号。

为了正确实现这个过程，我们必须定义一个有清晰状态和过渡的有限状态机。这样我们就可以正确管理握手过程中的所有极端情况。

![finite state machine diagram](/data/attachment/album/202001/13/151307dqmm9hm5yppcxpcq.png "finite state machine diagram")

会话 ID 由握手的初始化程序生成。一个完整的交换顺序如下：

1. 发送者发送一个 `SYN(ID)` 数据包。
2. 接收者存储接收到的 `ID` 并发送一个 `SYN-ACK(ID)`。
3. 发送者接收到 `SYN-ACK(ID)` 并发送一个 `ACK(ID)`。它还发送一个从序列号 0 开始的数据包。
4. 接收者检查最后接收到的 `ID`，如果 ID 匹配，则接受 `ACK(ID)`。它还开始接受序列号为 0 的数据包。

### 处理状态超时

基本上，每种状态下你都需要处理最多三种类型的事件：链接事件、数据包事件和超时事件。这些事件会并发地出现，因此你必须正确处理并发。

* 链接事件包括网络连接或网络断开的变化，相应的初始化一个链接会话或断开一个已建立的会话。
* 数据包事件是控制数据包（`SYN`/`SYN-ACK`/`ACK`）或只是探测响应。
* 超时事件在当前会话状态的预定超时时间到期后触发。

这里面临的最主要的问题是如何处理并发的超时到期和其他事件。这里很容易陷入死锁和资源竞争的陷阱。

### 第一种方法

本项目使用的语言是 [Golang](https://golang.org/)。它确实提供了原生的同步机制，如自带的通道和锁，并且能够使用轻量级线程来进行并发处理。

![gophers hacking together](/data/attachment/album/202001/13/151314tx43zcpl3rrg1fta.png "gophers hacking together")

*gopher 们聚众狂欢*

首先，你可以设计两个分别表示我们的会话和超时处理程序的结构体。

```
type Session struct {
  State SessionState
  Id SessionId
  RemoteIp string
}

type TimeoutHandler struct {
  callback func(Session)
  session Session
  duration int
  timer *timer.Timer
}
```

`Session` 标识连接会话，内有表示会话 ID、临近的连接点的 IP 和当前会话状态的字段。

`TimeoutHandler` 包含回调函数、对应的会话、持续时间和指向调度计时器的指针。

每一个临近连接点的会话都包含一个保存调度 `TimeoutHandler` 的全局映射。

```
SessionTimeout map[Session]*TimeoutHandler
```

下面方法注册和取消超时：

```
// schedules the timeout callback function.
func (timeout* TimeoutHandler) Register() {
  timeout.timer = time.AfterFunc(time.Duration(timeout.duration) * time.Second, func() {
    timeout.callback(timeout.session)
  })
}

func (timeout* TimeoutHandler) Cancel() {
  if timeout.timer == nil {
    return
  }
  timeout.timer.Stop()
}
```

你可以使用类似下面的方法来创建和存储超时：

```
func CreateTimeoutHandler(callback func(Session), session Session, duration int) *TimeoutHandler {
  if sessionTimeout[session] == nil {
    sessionTimeout[session] := new(TimeoutHandler)
  }

  timeout = sessionTimeout[session]
  timeout.session = session
  timeout.callback = callback
  timeout.duration = duration
  return timeout
}
```

超时处理程序创建后，会在经过了设置的 `duration` 时间（秒）后执行回调函数。然而，有些事件会使你重新调度一个超时处理程序（与 `SYN` 状态时的处理一样，每 3 秒一次）。

为此，你可以让回调函数重新调度一次超时：

```
func synCallback(session Session) {
  sendSynPacket(session)

  // reschedules the same callback.
  newTimeout := NewTimeoutHandler(synCallback, session, SYN_TIMEOUT_DURATION)
  newTimeout.Register()

  sessionTimeout[state] = newTimeout
}
```

这次回调在新的超时处理程序中重新调度自己，并更新全局映射 `sessionTimeout`。

### 数据竞争和引用

你的解决方案已经有了。可以通过检查计时器到期后超时回调是否执行来进行一个简单的测试。为此，注册一个超时，休眠 `duration` 秒，然后检查是否执行了回调的处理。执行这个测试后，最好取消预定的超时时间（因为它会重新调度），这样才不会在下次测试时产生副作用。

令人惊讶的是，这个简单的测试发现了这个解决方案中的一个问题。使用 `cancel` 方法来取消超时并没有正确处理。以下顺序的事件会导致数据资源竞争：

1. 你有一个已调度的超时处理程序。
2. 线程 1：
	1. 你接收到一个控制数据包，现在你要取消已注册的超时并切换到下一个会话状态（如发送 `SYN` 后接收到一个 `SYN-ACK`）
	2. 你调用了 `timeout.Cancel()`，这个函数调用了 `timer.Stop()`。（请注意，Golang 计时器的停止不会终止一个已过期的计时器。）
3. 线程 2：
	1. 在取消调用之前，计时器已过期，回调即将执行。
	2. 执行回调，它调度一次新的超时并更新全局映射。
4. 线程 1：
	1. 切换到新的会话状态并注册新的超时，更新全局映射。

两个线程并发地更新超时映射。最终结果是你无法取消注册的超时，然后你也会丢失对线程 2 重新调度的超时的引用。这导致处理程序在一段时间内持续执行和重新调度，出现非预期行为。

### 锁也解决不了问题

使用锁也不能完全解决问题。如果你在处理所有事件和执行回调之前加锁，它仍然不能阻止一个过期的回调运行：

```
func (timeout* TimeoutHandler) Register() {
  timeout.timer = time.AfterFunc(time.Duration(timeout.duration) * time._Second_, func() {
    stateLock.Lock()
    defer stateLock.Unlock()

    timeout.callback(timeout.session)
  })
}
```

现在的区别就是全局映射的更新是同步的，但是这还是不能阻止在你调用 `timeout.Cancel()` 后回调的执行 —— 这种情况出现在调度计时器过期了但是还没有拿到锁的时候。你还是会丢失一个已注册的超时的引用。

### 使用取消通道

你可以使用取消通道，而不必依赖不能阻止到期的计时器执行的 golang 函数 `timer.Stop()`。

这是一个略有不同的方法。现在你可以不用再通过回调进行递归地重新调度；而是注册一个死循环，这个循环接收到取消信号或超时事件时终止。

新的 `Register()` 产生一个新的 go 线程，这个线程在超时后执行你的回调，并在前一个超时执行后调度新的超时。返回给调用方一个取消通道，用来控制循环的终止。

```
func (timeout *TimeoutHandler) Register() chan struct{} {
  cancelChan := make(chan struct{})

  go func () {
    select {
    case _ = <- cancelChan:
      return
    case _ = <- time.AfterFunc(time.Duration(timeout.duration) * time.Second):
      func () {
        stateLock.Lock()
        defer stateLock.Unlock()

        timeout.callback(timeout.session)
      } ()
    }
  } ()

  return cancelChan
}

func (timeout* TimeoutHandler) Cancel() {
  if timeout.cancelChan == nil {
    return
  }
  timeout.cancelChan <- struct{}{}
}
```

这个方法给你注册的所有超时提供了取消通道。一个取消调用向通道发送一个空结构体并触发取消操作。然而，这并不能解决前面的问题；可能在你通过通道取消之前以及超时线程拿到锁之前，超时时间就已经到了。

这里的解决方案是，在拿到锁**之后**，检查一下超时范围内的取消通道。

```
  case _ = <- time.AfterFunc(time.Duration(timeout.duration) * time.Second):
    func () {
      stateLock.Lock()
      defer stateLock.Unlock()

      select {
      case _ = <- handler.cancelChan:
        return
      default:
        timeout.callback(timeout.session)
      }
    } ()
  }
```

最终，这可以确保在拿到锁之后执行回调，不会触发取消操作。

### 小心死锁

这个解决方案看起来有效；但是还是有个隐患：[死锁](https://en.wikipedia.org/wiki/Deadlock)。

请阅读上面的代码，试着自己找到它。考虑下描述的所有函数的并发调用。

这里的问题在取消通道本身。我们创建的是无缓冲通道，即发送的是阻塞调用。当你在一个超时处理程序中调用取消函数时，只有在该处理程序被取消后才能继续处理。问题出现在，当你有多个调用请求到同一个取消通道时，这时一个取消请求只被处理一次。当多个事件同时取消同一个超时处理程序时，如连接断开或控制包事件，很容易出现这种情况。这会导致死锁，可能会使应用程序停机。

![gophers on a wire, talking](/data/attachment/album/202001/13/151316k1dj066zm1y3yq0f.jpg "gophers on a wire, talking")

*有人在听吗？*

（已获得 Trevor Forrey 授权。）

这里的解决方案是创建通道时指定缓存大小至少为 1，这样向通道发送数据就不会阻塞，也显式地使发送变成非阻塞的，避免了并发调用。这样可以确保取消操作只发送一次，并且不会阻塞后续的取消调用。

```
func (timeout* TimeoutHandler) Cancel() {
  if timeout.cancelChan == nil {
    return
  }

  select {
  case timeout.cancelChan <- struct{}{}:
  default:
    // can’t send on the channel, someone has already requested the cancellation.
  }
}
```

### 总结

在实践中你学到了并发操作时出现的常见错误。由于其不确定性，即使进行大量的测试，也不容易发现这些问题。下面是我们在最初的实现中遇到的三个主要问题：

#### 在非同步的情况下更新共享数据

这似乎是个很明显的问题，但如果并发更新发生在不同的位置，就很难发现。结果就是数据竞争，由于一个更新会覆盖另一个，因此对同一数据的多次更新中会有某些更新丢失。在我们的案例中，我们是在同时更新同一个共享映射里的调度超时引用。（有趣的是，如果 Go 检测到在同一个映射对象上的并发读写，会抛出致命错误 — 你可以尝试下运行 Go 的[数据竞争检测器](https://golang.org/doc/articles/race_detector.html)）。这最终会导致丢失超时引用，且无法取消给定的超时。当有必要时，永远不要忘记使用锁。

![gopher assembly line](/data/attachment/album/202001/13/151317woonkapqz7punqoz.jpg "gopher assembly line")

*不要忘记同步 gopher 们的工作*

#### 缺少条件检查

在不能仅依赖锁的独占性的情况下，就需要进行条件检查。我们遇到的场景稍微有点不一样，但是核心思想跟[条件变量](https://en.wikipedia.org/wiki/Monitor_%28synchronization%29#Condition_variables)是一样的。假设有个一个生产者和多个消费者使用一个共享队列的经典场景，生产者可以将一个元素添加到队列并唤醒所有消费者。这个唤醒调用意味着队列中的数据是可访问的，并且由于队列是共享的，消费者必须通过锁来进行同步访问。每个消费者都可能拿到锁；然而，你仍然需要检查队列中是否有元素。因为在你拿到锁的瞬间并不知道队列的状态，所以还是需要进行条件检查。

在我们的例子中，超时处理程序收到了计时器到期时发出的“唤醒”调用，但是它仍需要检查是否已向其发送了取消信号，然后才能继续执行回调。

![gopher boot camp](/data/attachment/album/202001/13/151320iku72a0b8582lzyu.png "gopher boot camp")

*如果你要唤醒多个 gopher，可能就需要进行条件检查*

#### 死锁

当一个线程被卡住，无限期地等待一个唤醒信号，但是这个信号永远不会到达时，就会发生这种情况。死锁可以通过让你的整个程序停机来彻底杀死你的应用。

在我们的案例中，这种情况的发生是由于多次发送请求到一个非缓冲且阻塞的通道。这意味着向通道发送数据只有在从这个通道接收完数据后才能返回。我们的超时线程循环迅速从取消通道接收信号；然而，在接收到第一个信号后，它将跳出循环，并且再也不会从这个通道读取数据。其他的调用会一直被卡住。为避免这种情况，你需要仔细检查代码，谨慎处理阻塞调用，并确保不会发生线程饥饿。我们例子中的解决方法是使取消调用成为非阻塞调用 — 我们不需要阻塞调用。

---

via: <https://opensource.com/article/19/12/go-common-pitfalls>

作者：[Eduardo Ferreira](https://opensource.com/users/edufgf) 选题：[lujun9972](https://github.com/lujun9972) 译者：[lxbwolf](https://github.com/lxbwolf) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


PaperWM：GNOME 下的平铺窗口管理

Summary: 我使用 Gnome 已有很长时间了，但是我仍然有点想念平铺窗口管理器。六个月前，一个朋友告诉我有关 PaperWM 的消息，它使你可以在 Gnome 中平铺窗口！我立即安装了它，并从那时起我一直在使用它。

停止注册时什么梗

![](/data/attachment/album/202001/13/212936hsk572f4jrsj2jsh.jpg)

当我开始在个人计算机上使用 Linux 时，首先令我兴奋的就是轻巧的窗口管理器，这主要是因为当时我的笔记本电脑只有有 32MB 的内存，其它的都运行不了了。

接着我开始接触 [xmonad](https://xmonad.org/) 之类的平铺窗口管理器！我可以用键盘管理窗口了！它们是如此之快！我可以通过编写 Haskell 程序来配置 xmonad！我可以用各种有趣的方式自定义所有内容（例如使用 [dmenu](https://wiki.archlinux.org/index.php/Dmenu) 作为启动器）！这些年来，我用过 3、4 个不同的平铺窗口管理器，它们都很有趣。

大约 6 年前，我觉得配置平铺窗口管理器对我来说不再是一件有趣的事情，因此转而使用 Ubuntu 桌面环境 Gnome。（现在，我的笔记本电脑中的内存增加了 500 倍，这要快得多 :) ）

我使用 Gnome 已有很长时间了，但是我仍然有点想念平铺窗口管理器。六个月前，一个朋友告诉我有关 [PaperWM](https://github.com/paperwm/PaperWM) 的消息，它使你可以在 Gnome 中平铺窗口！我立即安装了它，并从那时起我一直在使用它。

### PaperWM：Gnome 下的平铺窗口管理

[PaperWM](https://github.com/paperwm/PaperWM) 的基本思想是：你想继续使用 Gnome（因为在 Gnome 中各种任务都能完成），但是你也希望使用平铺窗口管理器。

它是一个 Gnome 扩展程序（而不是一个独立的窗口管理器），并且使用 Javascript。

### “Paper” 表示你的所有窗户都在一行中

PaperWM 的主要想法是将所有窗口排成一行，这实际上与传统的平铺窗口管理器大不相同，在传统的平铺窗口管理器中，你可以按任意方式平铺窗口。这是我写这篇博客时在几个窗口之间切换/调整大小的 gif 图像（有一个浏览器和两个终端窗口）：

![](/data/attachment/album/202001/13/213028tt78epw18h258w12.gif)

PaperWM 的 Github README 链接了此视频：<http://10gui.com/video/>，它描述为一个类似的”线性窗口管理器“。

我以前从未听说过这种组织窗口的方式，但是我喜欢它的简单性。如果要查找特定的窗口，只需向左/向右移动，直到找到它。

### 我在 PaperWM 中所做的一切

还有很多其他功能，但这是我使用的功能：

* 在窗口之间左右移动（`Super + ,`、`Super + .`）
* 按顺序向左/向右移动窗口（`Super+Shift+,`、`Super+Shift+.`）
* 全屏显示窗口（`Super + f`）
* 缩小窗口（`Super + r`）

### 我喜欢不需要配置的工具

我在笔记本上使用 PaperWM 已经 6 个月了，我真的很喜欢它！即使它是可配置（通过编写 Javascript 配置文件），我也非常欣赏它，它自带我想要的功能，我无需研究如何去配置。

[fish shell](https://jvns.ca/blog/2017/04/23/the-fish-shell-is-awesome/) 是另一个类似的令人愉悦的工具，我基本上没有配置 fish（除了设置环境变量等），我真的很喜欢它的默认功能。

---

via: <https://jvns.ca/blog/2020/01/05/paperwm/>

作者：[Julia Evans](https://jvns.ca/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


在你的 Python 游戏中模拟引力

>
> 学习如何使用 Python 的 Pygame 模块编程电脑游戏，并开始操作引力。
>
>
>

![](/data/attachment/album/202001/13/223923k6t4zmvvmo6tfmmg.png)

真实的世界充满了运动和生活。物理学使得真实的生活如此忙碌和动态。物理学是物质在空间中运动的方式。既然一个电脑游戏世界没有物质，它也就没有物理学规律，使用游戏程序员不得不*模拟*物理学。

从大多数电脑游戏来说，这里基本上仅有两个方面的物理学是重要的：引力和碰撞。

当你[添加一个敌人](/article-10883-1.html)到你的游戏中时，你实现了一些碰撞检测，但是这篇文章要添加更多的东西，因为引力需要碰撞检测。想想为什么引力可能涉及碰撞。如果你不能想到任何原因，不要担心 —— 它会随着你开发示例代码工作而且显然。

在真实世界中的引力是有质量的物体来相互吸引的倾向性。物体（质量）越大，它施加越大的引力作用。在电脑游戏物理学中，你不必创建质量足够大的物体来证明引力的正确；你可以在电脑游戏世界本身中仅编程一个物体落向假设的最大的对象的倾向。

### 添加一个引力函数

记住你的玩家已经有了一个决定动作的属性。使用这个属性来将玩家精灵拉向屏幕底部。

在 Pygame 中，较高的数字更接近屏幕的底部边缘。

在真实的世界中，引力影响一切。然而，在平台游戏中，引力是有选择性的 —— 如果你添加引力到你的整个游戏世界，你的所有平台都将掉到地上。反之，你可以仅添加引力到你的玩家和敌人精灵中。

首先，在你的 `Player` 类中添加一个 `gravity` 函数：

```
    def gravity(self):
        self.movey += 3.2 # 玩家掉落的多快
```

这是一个简单的函数。首先，不管你的玩家是否想运动，你设置你的玩家垂直运动。也就是说，你已经编程你的玩家总是在下降。这基本上就是引力。

为使引力函数生效，你必须在你的主循环中调用它。这样，当每一个处理循环时，Python 都应用下落运动到你的玩家。

在这代码中，添加第一行到你的循环中：

```
    player.gravity() # 检查引力
    player.update()
```

启动你的游戏来看看会发生什么。要注意，因为它发生的很快：你是玩家从天空上下落，马上掉出了你的游戏屏幕。

你的引力模拟是工作的，但是，也许太好了。

作为一次试验，尝试更改你玩家下落的速度。

### 给引力添加一个地板

你的游戏没有办法发现你的角色掉落出世界的问题。在一些游戏中，如果一个玩家掉落出世界，该精灵被删除，并在某个新的位置重生。在另一些游戏中，玩家会丢失分数或一条生命。当一个玩家掉落出世界时，不管你想发生什么，你必须能够侦测出玩家何时消失在屏幕外。

在 Python 中，要检查一个条件，你可以使用一个 `if` 语句。

你必需查看你玩家**是否**正在掉落，以及你的玩家掉落的程度。如果你的玩家掉落到屏幕的底部，那么你可以做*一些事情*。简化一下，设置玩家精灵的位置为底部边缘上方 20 像素。

使你的 `gravity` 函数看起来像这样：

```
    def gravity(self):
        self.movey += 3.2 # 玩家掉落的多快

        if self.rect.y > worldy and self.movey >= 0:
            self.movey = 0
            self.rect.y = worldy-ty
```

然后，启动你的游戏。你的精灵仍然下落，但是它停在屏幕的底部。不过，你也许不能*看到*你在地面层之上的精灵。一个简单的解决方法是，在精灵碰撞游戏世界的底部后，通过添加另一个 `-ty` 到它的新 Y 位置，从而使你的精灵弹跳到更高处：

```
    def gravity(self):
        self.movey += 3.2 # 玩家掉落的多快

        if self.rect.y > worldy and self.movey >= 0:
            self.movey = 0
            self.rect.y = worldy-ty-ty
```

现在你的玩家在屏幕底部弹跳，恰好在你地面精灵上面。

你的玩家真正需要的是反抗引力的方法。引力问题是，你不能反抗它，除非你有一些东西来推开引力作用。因此，在接下来的文章中，你将添加地面和平台碰撞以及跳跃能力。在这期间，尝试应用引力到敌人精灵。

到目前为止，这里是全部的代码：

```
#!/usr/bin/env python3
# draw a world
# add a player and player control
# add player movement
# add enemy and basic collision
# add platform
# add gravity

# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

import pygame
import sys
import os

'''
Objects
'''

class Platform(pygame.sprite.Sprite):
    # x location, y location, img width, img height, img file
    def __init__(self,xloc,yloc,imgw,imgh,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img)).convert()
        self.image.convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

class Player(pygame.sprite.Sprite):
    '''
    Spawn a player
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.score = 1
        self.images = []
        for i in range(1,9):
            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect  = self.image.get_rect()

    def gravity(self):
        self.movey += 3.2 # how fast player falls

        if self.rect.y > worldy and self.movey >= 0:
            self.movey = 0
            self.rect.y = worldy-ty-ty

    def control(self,x,y):
        '''
        control player movement
        '''
        self.movex += x
        self.movey += y

    def update(self):
        '''
        Update sprite position
        '''

        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # moving left
        if self.movex < 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[self.frame//ani]

        # moving right
        if self.movex > 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[(self.frame//ani)+4]

        # collisions
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in enemy_hit_list:
            self.health -= 1
            print(self.health)

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.health -= 1
            print(self.health)

class Enemy(pygame.sprite.Sprite):
    '''
    Spawn an enemy
    '''
    def __init__(self,x,y,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img))
        #self.image.convert_alpha()
        #self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        '''
        enemy movement
        '''
        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1

class Level():
    def bad(lvl,eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0],eloc[1],'yeti.png') # spawn enemy
            enemy_list = pygame.sprite.Group() # create enemy group
            enemy_list.add(enemy)              # add enemy to group

        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list

    def loot(lvl,lloc):
        print(lvl)

    def ground(lvl,gloc,tx,ty):
        ground_list = pygame.sprite.Group()
        i=0
        if lvl == 1:
            while i < len(gloc):
                ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png')
                ground_list.add(ground)
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return ground_list

    def platform(lvl,tx,ty):
        plat_list = pygame.sprite.Group()
        ploc = []
        i=0
        if lvl == 1:
            ploc.append((0,worldy-ty-128,3))
            ploc.append((300,worldy-ty-256,3))
            ploc.append((500,worldy-ty-128,4))

            while i < len(ploc):
                j=0
                while j <= ploc[i][2]:
                    plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'ground.png')
                    plat_list.add(plat)
                    j=j+1
                print('run' + str(i) + str(ploc[i]))
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return plat_list

'''
Setup
'''
worldx = 960
worldy = 720

fps = 40 # frame rate
ani = 4  # animation cycles
clock = pygame.time.Clock()
pygame.init()
main = True

BLUE  = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)
ALPHA = (0,255,0)

world = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = world.get_rect()
player = Player() # spawn player
player.rect.x = 0
player.rect.y = 0
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10 # how fast to move

eloc = []
eloc = [200,20]
gloc = []
#gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630]
tx = 64 #tile size
ty = 64 #tile size

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1

enemy_list = Level.bad( 1, eloc )
ground_list = Level.ground( 1,gloc,tx,ty )
plat_list = Level.platform( 1,tx,ty )

'''
Main loop
'''
while main == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
            main = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                print("LEFT")
                player.control(-steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                print("RIGHT")
                player.control(steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

            if event.key == ord('q'):
                pygame.quit()
                sys.exit()
                main = False

    world.blit(backdrop, backdropbox)
    player.gravity() # check gravity
    player.update()
    player_list.draw(world)
    enemy_list.draw(world)
    ground_list.draw(world)
    plat_list.draw(world)
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)
```

---

这是仍在进行中的关于使用 [Pygame](https://www.pygame.org) 模块来在 [Python 3](https://www.python.org/) 在创建电脑游戏的第七部分。先前的文章是：

* [通过构建一个简单的掷骰子游戏去学习怎么用 Python 编程](/article-9071-1.html)
* [使用 Python 和 Pygame 模块构建一个游戏框架](/article-10850-1.html)
* [如何在你的 Python 游戏中添加一个玩家](/article-10858-1.html)
* [用 Pygame 使你的游戏角色移动起来](/article-10874-1.html)
* [如何向你的 Python 游戏中添加一个敌人](/article-10883-1.html)
* [在 Pygame 游戏中放置平台](/article-10902-1.html)

---

via: <https://opensource.com/article/19/11/simulate-gravity-python>

作者：[Seth Kenlon](https://opensource.com/users/seth) 选题：[lujun9972](https://github.com/lujun9972) 译者：[robsean](https://github.com/robsean) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 Bash 脚本发送包含几天内到期的用户账号列表的电子邮件

Summary: 大多数团队忘记更改服务帐户密码，这可能导致日常工作的中断，即使他们配置有基于 SSH 密钥的身份验证。如果用户帐户密码过期，基于SSH密钥的身份验证和 cronjob 将不起作用。

![](/data/attachment/album/202001/13/232047vfxkrr47mn7eapda.jpg)

密码强制策略对所有操作系统和应用程序都是通用的。如果要[在 Linux 上实现密码强制策略](/article-11709-1.html)，请参阅以下文章。

默认情况下，大多数公司都会强制执行密码强制策略，但根据公司的要求，密码的时间周期会有所不同。通常每个人都使用 90 天的密码周期。用户只会在他们使用的一些服务器上[更改密码](https://www.2daygeek.com/linux-passwd-chpasswd-command-set-update-change-users-password-in-linux-using-shell-script/)，而不会在他们不经常使用的服务器上更改密码。

特别地，大多数团队忘记更改服务帐户密码，这可能导致日常工作的中断，即使他们配置有[基于 SSH 密钥的身份验证](https://www.2daygeek.com/configure-setup-passwordless-ssh-key-based-authentication-linux/)。如果用户帐户密码过期，基于SSH密钥的身份验证和 [cronjob](https://www.2daygeek.com/linux-crontab-cron-job-to-schedule-jobs-task/) 将不起作用。

为了避免这种情况，我们创建了一个 [shell 脚本](https://www.2daygeek.com/category/shell-script/)来向你发送 10 天内到期的用户帐户列表。

本教程中包含两个 [bash 脚本](https://www.2daygeek.com/category/bash-script/)可以帮助你收集系统中用户到期天数的信息。

### 1) 检查 10 天后到期的用户帐户列表

此脚本将帮助你在终端上检查 10 天内到期的用户帐户列表。

```
# vi /opt/script/user-password-expiry.sh
```

```
#!/bin/sh
/tmp/user-expiry-1.txt
/tmp/user-expiry.txt
echo "-------------------------------------------------"
echo "UserName  The number of days the password expires"
echo "-------------------------------------------------"
for usern in u1 u2 u3 u4
do
today=$(date +%s)
userexpdate=$(chage -l $usern | grep 'Password expires' |cut -d: -f2)
passexp=$(date -d "$userexpdate" "+%s")
exp=`expr \( $passexp - $today \)`
expday=`expr \( $exp / 86400 \)`
echo "$usern $expday" >> /tmp/user-expiry.txt
done
cat /tmp/user-expiry.txt | awk '$2 <= 10' > /tmp/user-expiry-1.txt
cat /tmp/user-expiry-1.txt | column -t
```

将文件 `user-password-expiry.sh` 设置为可执行的 Linux 文件权限。

```
# chmod +x /opt/script/user-password-expiry.sh
```

你将得到如下输出，但用户与天数可能不同。

```
# sh /opt/script/user-password-expiry.sh

-------------------------------------------------
UserName  The number of days the password expires
-------------------------------------------------
u1         -25
u2          9
u3          3
u4          5
```

### 2) 发送包含 10 天内到期的用户帐户列表的电子邮件

此脚本将发送一封包含 10 天内到期的用户帐户列表的邮件。

```
# vi /opt/script/user-password-expiry-mail.sh
```

```
#!/bin/sh
SUBJECT="Information About User Password Expiration on "`date`""
MESSAGE="/tmp/user-expiry.txt"
MESSAGE1="/tmp/user-expiry-1.txt"
TO="magesh.m@rentacenter.com"
echo "-------------------------------------------------" >> $MESSAGE1
echo "UserName  The number of days the password expires" >> $MESSAGE1
echo "-------------------------------------------------" >> $MESSAGE1
for usern in u1 u2 u3 u4
do
today=$(date +%s)
userexpdate=$(chage -l $usern | grep 'Password expires' |cut -d: -f2)
passexp=$(date -d "$userexpdate" "+%s")
exp=`expr \( $passexp - $today \)`
expday=`expr \( $exp / 86400 \)`
echo "$usern $expday" >> $MESSAGE
done
cat $MESSAGE | awk '$2 <= 10' >> $MESSAGE1
mail -s "$SUBJECT" "$TO" < $MESSAGE1
rm $MESSAGE
rm $MESSAGE1
```

将文件 `user-password-expiry-mail.sh` 设置为可执行的 Linux 文件权限。

```
# chmod +x /opt/script/user-password-expiry-mail.sh
```

最后，添加一个 [cronjob](https://www.2daygeek.com/linux-crontab-cron-job-to-schedule-jobs-task/) 去自动执行脚本。每天早上 8 点运行一次。

```
# crontab -e
0 8 * * * /bin/bash /opt/script/user-password-expiry-mail.sh
```

你将收到一封与第一个脚本输出类似的电子邮件。

---

via: <https://www.2daygeek.com/bash-script-to-check-user-account-password-expiry-linux/>

作者：[Magesh Maruthamuthu](https://www.2daygeek.com/author/magesh/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[qianmingtian](https://github.com/qianmingtian) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


写到世界充满爱：专访 RSSHub 作者 DIYgod

Summary: DIYgod 是一个年轻有为的开发者，是在 GitHub 拥有一万星标（本文发表时）的 RSSHub 的创始人，也是 APlayer（4k 星标）、DPlayer （8k 星标）等开源项目的创建者。

膜拜大佬

膜拜，大佬给我前进的动力  O(∩_∩)O

莫名其妙刷到大佬，仅几年来第一次耐心认真的查找阅读了与大佬相关的很多信息，自觉获益良多

DIY God是我的神！！！！！

![](/data/attachment/album/202001/15/103430v5s4iznczy0zxy3p.jpg)

拥有一个很萌的头像的 [DIYgod](https://github.com/DIYgod) 是一个当前任职于 B 站的年轻开发者，他是在 GitHub 拥有一万星标（本文发表时）的 [RSSHub](https://github.com/DIYgod/RSSHub) 的创始人，也是 [APlayer](https://github.com/MoePlayer/APlayer)（4.4k 星标）、[DPlayer](https://github.com/MoePlayer/DPlayer) （8k 星标）等开源项目的创始人。在我所认识的开源开发者当中，DIYgod 是一个很优秀的开源社区贡献者，所以今天我们邀请到了 DIYgod 来参加我们的穿山甲专访。

![](/data/attachment/album/202001/15/100956pn8yqpm7zr9dk3nj.jpg)

*DIYgod 很有 B 站风格的头像*

---

**问：您可以先自我介绍一下么？**

DIYgod：Hi，大家好，我是 DIYgod，一名爱好开源的 JavaScript 开发者 —— **写代码是热爱，写到世界充满爱**。我的梦想是成为**一名可以养活自己的自由职业者**。

**问：目前你已经是自由职业状态，还是说仍然在企业工作呢？**

DIYgod：现在在哔哩哔哩（B 站）做前端。

**问：能否向读者介绍一下你的开源项目 RSSHub 呢？**

DIYgod：RSSHub 是一个用来生成 RSS 订阅源的工具，它可以给任何奇奇怪怪的内容生成 RSS 订阅源，实现万物皆可 RSS。RSSHub 正在**借助于开源社区的力量快速发展中**，目前已适配了数百家网站的上千项内容。使用时只需要简单的编辑下地址即可获得需要的订阅源。

![](/data/attachment/album/202001/15/101626cvgf9vkqvhyzjy9f.jpg)

**问：对于目前的很多人来说，RSS 已经鲜为人知，现在很多新生代的互联网用户已经不再使用 RSS，他们可能更习惯于使用信息流应用。你当初因为什么原因选择做了 RSSHub ，是有什么契机么？**

DIYgod：我本身是一个 RSS 重度用户，之前关注了几个有意思的微博博主，但经常打开微博去刷更新太麻烦了，就写了个简单的 Node.JS 脚本生成 RSS 加到自己的订阅里，后来又写了哔哩哔哩的 RSS、网易云音乐的 RSS，越来越多，最后就干脆把它们合成了一个项目，取名 RSSHub。

**问：说起来，你其实是将微博的信息流推送的形式，借助于自己的编程能力，转化为 RSS 的被动拉取的形态。那对于 RSS 和 现在的信息流应用，你有什么看法么？**

DIYgod：信息流应用基本都有自己的一套推荐算法，受益于此，可以获得更轻松愉快的阅读体验；但另一方面来看，用户丧失了内容选择的主动权，看到的不一定是自己真正想看的，稍不注意也会消耗自己大量的时间，阅读效率更低。RSS 可以选择自己真正想看的内容，阅读效率更高，但这也导致使用门槛比信息流应用高了很多。信息流应用的另一个问题是它无法集中地收取信息，时不时地打开微博、Twitter、YouTube、哔哩哔哩，去翻看我关注的人有没有更新，实在是一件痛苦的事。最后是 RSS 可以做到没有遗漏地收取信息，而信息流应用很容易遗漏。

**问：可以看出来，你是一个重度的 RSS 用户，不仅仅是用户，更是为 RSS 生态添砖加瓦。你自己平时都是怎么样应用 RSS 的呢？**

DIYgod：大家都知道 RSS 是一种用来做消息聚合的格式规范，有着更高的阅读效率、更好的阅读体验、可以掌握主动权等等优点，但它的用途一直被大家低估，除了最常用的在 RSS 阅读器里使用，还可以通过 BT 客户端实现自动的 BT 下载用来追美剧或动漫、通过播客客户端订阅和收听播客、通过 IFTTT 与各种各样的东西联动等等。

我平时除了常规的使用 RSS 阅读器订阅，还会在群晖的 BT 客户端里订阅美剧的 RSS，这样美剧更新后 BT 客户端就会自动把最新一集下载到硬盘里，晚上下班回家打开电视就可以直接看了。

此外是自动下载我的 B 站投币视频，整个流程是“投币操作 -> RSS 更新 -> IFTTT 触发 Webhook -> 服务器下载”，实现方法在我的博客里有介绍：<https://diygod.me/download-webhook> 。

然后还有我的 Telegram 频道： <https://t.me/awesomeDIYgod> ，它通过 IFTTT 监听了很多 RSS 更新，有 DIYgod 的博客更新、DIYgod 的 PSN 奖杯、DIYgod 的 Twitter 更新、DIYgod 喜欢的网易云音乐、DIYgod 的 bilibili 投币视频等等，几乎包括了我的全部动态。

**问：说起来集中在一个地方收取信息，你怎么看曾经的“即刻”应用，即刻应用也可以关注特定的人、微博之类的，在一个地方查看所有的信息。**

DIYgod：我非常喜欢“即刻”，在“即刻”倒闭之前也一直在使用它，早期很像一个 RSS 阅读器，甚至真的可以订阅 RSS，但后来这些功能越来越淡化直至去掉了，取而代之的都是 UGC 内容了。

**问: RSSHub 里有非常多的“路由”，包括社交媒体、新媒体、论坛等。除了我们一般意义上的信息流转化 RSS 以外，RSSHub 还有非常多有意思的 Feed，比如高校教务处通知的 RSS Feed，就你自己而言，你最喜欢 RSSHub 中的哪一个条目？**

DIYgod：那当然是 “RSSHub 有新路由啦”。

**问：那么，除了 RSSHub，你还会使用哪些 RSS 生态中的工具呢？**

DIYgod：除了 RSS 阅读器和支持 RSS 的 BT 客户端，还有 IFTTT 和 Tiny Tiny RSS 及其插件。

**问：RSSHub 是一个基于 MIT 许可证开源的项目，你自己当初是怎么走上开源的“不归路”的呢？**

DIYgod：刚学前端的时候，为了练手写了几个很简单的小项目，然后把它们传到了 GitHub 上想着找工作时候可以用到，没想到真的有人会去用自己写的东西，收获了第一个<ruby> 提案 <rp>  （ </rp> <rt>  issue </rt> <rp>  ） </rp></ruby>，第一个星标，第一个拉取请求，就这样发现了其中的乐趣，打开了新世界的大门。

**问：RSSHub 是中国的个人开发者开源的项目中首屈一指的项目，获得了非常多的星标 ，也有很多贡献者，对于开源，你有什么想要告诉大家的么？或者说，在你看来，想要做好开源，最重要的是什么？**

DIYgod：希望大家没尝试过的都尝试一下，收获第一个星标，第一个拉取请求的快乐无法描述，不仅可以帮到别人，也可以快速地提升自己；最重要的是兴趣，开源项目需要投入大量的业余时间去更新维护，用爱发电，然后是持之以恒，挖一个坑很容易，但后续的更新维护也很重要。

**问：RSSHub 项目的社区化非常的高，有 300 多位贡献者，很多社区开源项目都难以获得这么多的社区贡献者，你是如何让这些来自全国的开发者相互协同的呢？**

DIYgod：我觉得这更多的是跟项目性质有关系，RSSHub 是一个需要大量人力来适配各种网站的规则的项目，可以参与的地方很多，参与门槛不高，又能获得非常积极的反馈。

1. 可以参与的地方很多：每个 RSS 路由都对应一个脚本，可以让很多人参与进来。
2. 参与门槛不高：脚本的编写难度不高，RSSHub 还有非常详细的开发文档，进一步降低了开发门槛，然后采用了统一代码规范，严格的自动化测试来避免出现问题。
3. 积极的反馈：可以很方便地自己动手制作自己想要的 RSS 源并分享给很多人用，同时在文档对应的 RSS 源也标记了路由作者的名字。

**问：现在有一个机会，你可以推荐一个东西给大家，你会推荐什么？可以是软件、可以是网络服务、可以是硬件，Everything is Ok.**

DIYgod：PS4 和 Switch — “No Game No Life”；跟 RSS 相关的再推荐一下“快知 APP”。

**问：大家在哪里可以找到你呢？**

DIYgod：

* GitHub：@DIYgod；
* Twitter：@DIYgod；
* 博客：diygod.me；
* Telegram频道：@awesomeDIYgod

---

### 关于穿山甲专访

![](/data/attachment/album/202001/15/102514o9h1nhgz1gx9xyfx.png)

“穿山甲专访”栏目是 Linux 中国社区推出的面向开源界、互联网技术圈的重要领军人物的系列采访，将为大家介绍中国开源领域中一些积极推动开源，谙熟开源思想的技术人，并辨析其思考、挖掘其动因，揭示其背后所发生的事情，为关注开源、有志于开源的企业和技术人标出一条路径。

取名为“穿山甲”寓意有二：取穿山甲挖掘、深入之意来象征技术进步和表征技术领袖的作用；穿山甲是珍稀保护动物，宣传公益。

如果你希望加入到穿山甲计划专访中，请访问 <https://jinshuju.net/f/9X8gvG> ，填写报名表。


使用 Linux seq 命令生成数字序列

>
> Linux 的 seq 命令可以以闪电般的速度生成数字列表，而且它也易于使用而且灵活。
>
>
>

![](/data/attachment/album/202001/15/112717drpb9nuwss84xebu.jpg)

在 Linux 中生成数字列表的最简单方法之一是使用 `seq`（<ruby> 系列 <rt>  sequence </rt></ruby>）命令。其最简单的形式是，`seq` 接收一个数字参数，并输出从 1 到该数字的列表。例如：

```
$ seq 5
1
2
3
4
5
```

除非另有指定，否则 `seq` 始终以 1 开头。你可以在最终数字前面插上不同数字开始一个序列。

```
$ seq 3 5
3
4
5
```

### 指定增量

你还可以指定增量步幅。假设你要列出 3 的倍数。指定起点（在此示例中为第一个 3 ），增量（第二个 3）和终点（18）。

```
$ seq 3 3 18
3
6
9
12
15
18
```

你可以选择使用负增量（即减量）将数字从大变小。

```
$ seq 18 -3 3
18
15
12
9
6
3
```

`seq` 命令也非常快。你或许可以在 10 秒内生成一百万个数字的列表。

```
$ time seq 1000000
1
2
3
…
…
999998
999999
1000000

real    0m9.290s  <== 9+ seconds
user    0m0.020s
sys     0m0.899s
```

### 使用分隔符

另一个非常有用的选项是使用分隔符。你可以插入逗号、冒号或其他一些字符，而不是在每行上列出单个数字。`-s` 选项后跟要使用的字符。

```
$ seq -s: 3 3 18
3:6:9:12:15:18
```

实际上，如果只是希望将数字列在一行上，那么可以使用空格代替默认的换行符。

```
$ seq -s' '  3 3 18
3 6 9 12 15 18
```

### 开始数学运算

从生成数字序列到进行数学运算似乎是一个巨大的飞跃，但是有了正确的分隔符，`seq` 可以轻松地传递给 `bc` 进行计算。例如：

```
$ seq -s* 5 | bc
120
```

该命令中发生了什么？让我们来看看。首先，`seq` 生成一个数字列表，并使用 `*` 作为分隔符。

```
$ seq -s* 5
1*2*3*4*5
```

然后，它将字符串传递给计算器（`bc`），计算器立即将数字相乘。你可以在不到一秒的时间内进行相当庞大的计算。

```
$ time seq -s* 117 | bc
39699371608087208954019596294986306477904063601683223011297484643104\
22041758630649341780708631240196854767624444057168110272995649603642\
560353748940315749184568295424000000000000000000000000000

real    0m0.003s
user    0m0.004s
sys     0m0.000s
```

### 局限性

你只能选择一个分隔符，因此计算将非常有限。而单独使用 `bc` 可进行更复杂的数学运算。此外，`seq` 仅适用于数字。要生成单个字母的序列，请改用如下命令：

```
$ echo {a..g}
a b c d e f g
```

---

via: <https://www.networkworld.com/article/3511954/generating-numeric-sequences-with-the-linux-seq-command.html>

作者：[Sandra Henry-Stocker](https://www.networkworld.com/author/Sandra-Henry_Stocker/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


piwheels 是如何为树莓派用户节省时间的

>
> 通过为树莓派提供预编译的 Python 包，piwheels 项目为用户节省了大量的时间和精力。
>
>
>

![rainbow colors on pinwheels in the sun](/data/attachment/album/202001/15/130559eef6f6e11w3cyecu.png "rainbow colors on pinwheels in the sun")

piwheels 自动为 Python 包索引 [PiPi](https://pypi.org/) 上的所有项目构建 Python wheels（预编译的 Python包），并使用了树莓派硬件以确保其兼容性。这意味着，当树莓派用户想要使用 `pip` 安装一个 Python 库时，他们会得到一个现成编译好的版本，并保证可以在树莓派上良好的工作。这使得树莓派用户更容易入门并开始他们的项目。

![Piwheels logo](/data/attachment/album/202001/15/130602uqi3h3dccmywaize.png "Piwheels logo")

当我在 2018 年 10 月写 [piwheels：为树莓派提供快速 Python 包安装](https://opensource.com/article/18/10/piwheels-python-raspberrypi)时，那时 piwheels 项目已经有一年了，并且已经证明了其为树莓派用户节省大量时间和精力。但当这个项目进入第二年时，它为树莓派提供了预编译的 Python 包做了更多工作。

![Raspberry Pi 4](/data/attachment/album/202001/15/130617kaq1wl5dtkbtbk93.jpg "Raspberry Pi 4")

### 它是怎么工作的

树莓派的主要操作系统 [Raspbian](https://www.raspberrypi.org/downloads/raspbian/) 预配置使用了 piwheels，所以用户不需要做任何特殊的事情就可以使用 piwheels。

配置文件（在 `/etc/pip.conf`）告诉 `pip` 使用 [piwheels.org](http://piwheels.org) 作*附加索引*，因此 `pip` 会首先查找 PyPI，然后查找 piwheels。piwheels 的网站被托管在一个树莓派 3 上，该项目构建的所有 wheels 都托管在该树莓派上。它每月提供 100 多万个软件包——这对于一台 35 美元的电脑来说还真不赖！

除了提供网站服务的主树莓派以外，piwheels 项目还使用其他七个树莓派来构建软件包。其中一些运行 Raspbian Jessie，为 Python 3.4 构建 wheels；另外一些运行 Raspbian Stretch 为 Python 3.5 构建；还有一些运行 Raspbian Buster 为 Python 3.7 构建。该项目通常不支持其他 Python 版本。还有一个“合适的服务器”——一台运行 Postgres 数据库的虚拟机。由于树莓派 3 只有 1GB 的内存，所以（非常大的）数据库不能在其上很好地运行，所以我们把它移到了虚拟机上。带 4GB 内存的树莓派 4 可能是合用的，所以我们将来可能会用到它。

这些树莓派都在“派云”中的 IPv6 网络上——这是一项由总部位于剑桥的托管公司 [Mythic Beasts](https://www.mythic-beasts.com/order/rpi) 提供的卓越服务。

![Mythic Beasts hosting service](/data/attachment/album/202001/15/130620zqgwxgwerph9x92x.png "Mythic Beasts hosting service")

### 下载和统计趋势

每次下载 piwheels 文件时，它都会记录在数据库中。这提供了对什么包最受欢迎以及人们使用什么 Python 版本和操作系统的统计。我们没有太多来自用户代理的信息，但是因为树莓派 1/Zero 的架构显示为 “armv6”，树莓派 2/¾ 显示为 “armv7”，所以我们可以将它们区分开来。

截至 2019 年 12 月中旬，从 piwheels 下载的软件包超过 1400 万个，仅 2019 年就有近 900 万个。

自项目开始以来最受欢迎的 10 个软件包是:

1. [pycparser](https://www.piwheels.org/project/pycparser)（821,060 个下载）
2. [PyYAML](https://www.piwheels.org/project/PyYAML)（366,979 个下载）
3. [numpy](https://www.piwheels.org/project/numpy)（354,531 个下载）
4. [cffi](https://www.piwheels.org/project/cffi)（336,982 个下载）
5. [MarkupSafe](https://www.piwheels.org/project/MarkupSafe)（318,878 个下载）
6. [future](https://www.piwheels.org/project/future)（282,349 个下载）
7. [aiohttp](https://www.piwheels.org/project/aiohttp)（277,046 个下载）
8. [cryptography](https://www.piwheels.org/project/cryptography)（276,167 个下载）
9. [home-assistant-frontend](https://www.piwheels.org/project/home-assistant-frontend)（266,667 个下载）
10. [multidict](https://www.piwheels.org/project/multidict)（256,185 个下载）

请注意，许多纯 Python 包，如 [urllib3](https://piwheels.org/project/urllib3/)，都是作为 PyPI 上的 wheels 提供的；因为这些是跨平台兼容的，所以通常不会从 piwheels 下载，因为 PyPI 优先。

随着时间的推移，我们也看到了使用哪些 Python 版本的趋势。这里显示了 Raspbian Buster 发布时从 3.5 版快速升级到了 Python 3.7:

![Data from piwheels on Python versions used over time](/data/attachment/album/202001/15/130622xlastfvfrr5uluoh.png "Data from piwheels on Python versions used over time")

你可以在我们的这篇 [统计博文](https://blog.piwheels.org/piwheels-stats-for-2019/) 看到更多的统计趋势。

### 节省的时间

每个包构建都被记录在数据库中，并且每个下载也被存储。交叉引用下载数和构建时间显示了节省了多少时间。一个例子是 numpy —— 最新版本大约需要 11 分钟来构建。

迄今为止，piwheels 项目已经为用户节省了总计超过 165 年的构建时间。按照目前的使用率，piwheels 项目每天可以节省 200 多天。

除了节省构建时间，拥有预编译的 wheels 也意味着人们不必安装各种开发工具来构建包。一些包需要其他 apt 包来访问共享库。弄清楚你需要哪一个可能会很痛苦，所以我们也让这一步变得容易了。首先，我们找到了这个过程，[在博客上记录了这个过程](https://blog.piwheels.org/how-to-work-out-the-missing-dependencies-for-a-python-package/)。然后，我们将这个逻辑添加到构建过程中，这样当构建一个 wheels 时，它的依赖关系会被自动计算并添加到包的项目页面中:

![numpy dependencies](/data/attachment/album/202001/15/130622zhjvhd43jxl3hkqn.png "numpy dependencies")

### piwheels 的下一步是什么？

今年，我们推出了项目页面（例如，[numpy](https://www.piwheels.org/project/numpy/)），这是一种非常有用的方式，可以让人们以人类可读的方式查找项目信息。它们还使人们更容易报告问题，例如 piwheels 中缺少一个项目，或者他们下载的包有问题。

2020 年初，我们计划对 piwheels 项目进行一些升级，以启用新的 JSON 应用编程接口，这样你就可以自动检查哪些版本可用，查找项目的依赖关系，等等。

下一次 Debian/Raspbian 升级要到 2021 年年中才会发生，所以在那之前我们不会开始为任何新的 Python 版本构建 wheels。

你可以在这个项目的[博客](https://blog.piwheels.org/)上读到更多关于 piwheels 的信息，我将在 2020 年初在那里发表一篇 2019 年的综述。你也可以在推特上关注 [@piwheels](https://twitter.com/piwheels)，在那里你可以看到每日和每月的统计数据以及任何达到的里程碑。

当然，piwheels 是一个开源项目，你可以在 [GitHub](https://github.com/piwheels/) 上看到整个项目源代码。

---

via: <https://opensource.com/article/20/1/piwheels>

作者：[Ben Nuttall](https://opensource.com/users/bennuttall) 选题：[lujun9972](https://github.com/lujun9972) 译者：[heguangzhi](https://github.com/heguangzhi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


外媒：华为的 Linux 发行版 openEuler 可以使用了！

Summary: 华为提供了一个基于 CentOS 的企业级 Linux 发行版 EulerOS。最近，华为发布了一个名为 openEuler 的 EulerOS 社区版。

辣子鸡

应该命名为 DollarOS, 比 CentOS 更多！

不用下了，支持ARM 64bit 普通X86-64 用不了！

不懂就别瞎逼逼，不知道分两种版本的吗，arm64和x86-64

>
> 华为提供了一个基于 CentOS 的企业级 Linux 发行版 EulerOS。最近，华为发布了一个名为 [openEuler](https://openeuler.org/en/) 的 EulerOS 社区版。
>
>
>

openEuler 的源代码也一同发布了。你在微软旗下的 GitHub 上找不到它——源代码可以在 [Gitee](https://gitee.com/openeuler) 找到，这是一个中文的 [GitHub 的替代品](https://itsfoss.com/github-alternatives/)。

它有两个独立的存储库，一个用于存储[源代码](https://gitee.com/openeuler)；另一个作为[软件包的源代码](https://gitee.com/src-openeuler)，存储有助于构建该操作系统的软件包。

![](/data/attachment/album/202001/16/105741tdqgzjqg2o2s7mlo.jpg)

openEuler 基础架构团队分享了他们使源代码可用的经验：

>
> 我们现在很兴奋。很难想象我们会管理成千上万的仓库。为了确保它们能被成功地编译，我们要感谢所有参与贡献的人。
>
>
>

### openEuler 是基于 CentOS 的 Linux 发行版

与 EulerOS 一样，openEuler OS 也是基于 [CentOS](https://www.centos.org/)，但华为技术有限公司为企业应用进一步开发了该操作系统。

它是为 ARM64 架构的服务器量身定做的，同时华为声称已经做了一些改变来提高其性能。你可以在[华为开发博客](https://developer.huaweicloud.com/en-us/euleros/euleros-introduction.html)上了解更多。

![](/data/attachment/album/202001/16/105745hamtndgugldeniil.jpg)

目前，根据 openEuler 的官方声明，有 50 多名贡献者为 openEuler 贡献了近 600 个提交。

贡献者们使源代码对社区可用成为可能。

值得注意的是，存储库还包括两个与之相关的新项目（或子项目），[iSulad](https://gitee.com/openeuler/iSulad) 和 A-Tune。

A-Tune 是一个基于 AI 的操作系统调优软件，iSulad 是一个轻量级的容器运行时守护进程，如在 [Gitee](https://gitee.com/openeuler) 中提到的那样，它是为物联网和云基础设施设计的。

另外，官方的[公告](https://openeuler.org/en/news/20200101.html)提到，这些系统是在华为云上通过脚本自动构建的。这确实十分有趣。

### 下载 openEuler

![](/data/attachment/album/202001/16/105746zsjqyjv3px1kxbjj.jpg)

到目前为止，你找不到它的英文文档，所以你必须等待或选择通过（贡献）[文档](https://gitee.com/openeuler/docs)来帮助他们。

你可以直接从它的[官方网站](https://openeuler.org/en/download.html)下载 ISO 来测试它:

* [下载 openEuler](https://openeuler.org/en/download.html)

### 你认为华为的 openEuler 怎么样?

据 cnTechPost 报道，华为曾宣布 EulerOS 将以新名字 openEuler 成为开源软件。

目前还不清楚 openEuler 是否会取代 EulerOS ，或者两者会像 CentOS（社区版）和 Red Hat（商业版）一样同时存在。

我还没有测试过它，所以我不能说 openEuler 是否适合英文用户。

你愿意试一试吗？如果你已经尝试过了，欢迎在下面的评论中告诉我你的体验。

---

via: <https://itsfoss.com/openeuler/>

作者：[Ankush Das](https://itsfoss.com/author/ankush/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[qianmingtian]((https://github.com/qianmingtian)) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


如何使用 Pyramid 和 Cornice 编写 Python Web API

>
> 使用 Pyramid 和 Cornice 构建和描述可扩展的 RESTful Web 服务。
>
>
>

![](/data/attachment/album/202001/16/120352fcgeeccvfgt8sfvc.jpg)

[Python](https://opensource.com/resources/python) 是一种高级的、面向对象的编程语言，它以其简单的语法而闻名。它一直是构建 RESTful API 的顶级编程语言之一。

[Pyramid](https://opensource.com/article/18/5/pyramid-framework) 是一个 Python Web 框架，旨在随着应用的扩展而扩展：这可以让简单的应用很简单，也可以增长为大型、复杂的应用。此外，Pyramid 为 PyPI （Python 软件包索引）提供了强大的支持。[Cornice](https://cornice.readthedocs.io/en/latest/) 为使用 Pyramid 构建和描述 RESTful Web 服务提供了助力。

本文将使用 Web 服务的例子来获取名人名言，来展示如何使用这些工具。

### 建立 Pyramid 应用

首先为你的应用创建一个虚拟环境，并创建一个文件来保存代码：

```
$ mkdir tutorial
$ cd tutorial
$ touch main.py
$ python3 -m venv env
$ source env/bin/activate
(env) $ pip3 install cornice twisted
```

### 导入 Cornice 和 Pyramid 模块

使用以下命令导入这些模块：

```
from pyramid.config import Configurator
from cornice import Service
```

### 定义服务

将引用服务定义为 `Service` 对象：

```
QUOTES = Service(name='quotes',
                 path='/',
                 description='Get quotes')
```

### 编写引用逻辑

到目前为止，这仅支持获取名言。用 `QUOTES.get` 装饰函数。这是将逻辑绑定到 REST 服务的方法：

```
@QUOTES.get()
def get_quote(request):
    return {
        'William Shakespeare': {
            'quote': ['Love all, trust a few, do wrong to none',
            'Some are born great, some achieve greatness, and some have greatness thrust upon them.']
    },
    'Linus': {
        'quote': ['Talk is cheap. Show me the code.']
        }
    }
```

请注意，与其他框架不同，装饰器*不会*更改 `get_quote` 函数。如果导入此模块，你仍然可以定期调用该函数并检查结果。

在为 Pyramid RESTful 服务编写单元测试时，这很有用。

### 定义应用对象

最后，使用 `scan` 查找所有修饰的函数并将其添加到配置中：

```
with Configurator() as config:
    config.include("cornice")
    config.scan()
    application = config.make_wsgi_app()
```

默认扫描当前模块。如果要扫描软件包中的所有模块，你也可以提供软件包的名称。

### 运行服务

我使用 Twisted 的 WSGI 服务器运行该应用，但是如果需要，你可以使用任何其他 [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) 服务器，例如 Gunicorn 或 uWSGI。

```
(env)$ python -m twisted web --wsgi=main.application
```

默认情况下，Twisted 的 WSGI 服务器运行在端口 8080 上。你可以使用 [HTTPie](https://opensource.com/article/19/8/getting-started-httpie) 测试该服务：

```
(env) $ pip install httpie
...
(env) $ http GET <http://localhost:8080/>
HTTP/1.1 200 OK
Content-Length: 220
Content-Type: application/json
Date: Mon, 02 Dec 2019 16:49:27 GMT
Server: TwistedWeb/19.10.0
X-Content-Type-Options: nosniff

{
    "Linus": {
        "quote": [
            "Talk is cheap. Show me the code."
        ]
    },
    "William Shakespeare": {
        "quote": [
            "Love all,trust a few,do wrong to none",
            "Some are born great, some achieve greatness, and some greatness thrust upon them."
        ]
    }
}
```

### 为什么要使用 Pyramid？

Pyramid 并不是最受欢迎的框架，但它已在 [PyPI](https://pypi.org/) 等一些引人注目的项目中使用。我喜欢 Pyramid，因为它是认真对待单元测试的框架之一：因为装饰器不会修改函数并且没有线程局部变量，所以可以直接从单元测试中调用函数。例如，需要访问数据库的函数将从通过 `request.config` 传递的 `request.config` 对象中获取它。这允许单元测试人员将模拟（或真实）数据库对象放入请求中，而不用仔细设置全局变量、线程局部变量或其他特定于框架的东西。

如果你正在寻找一个经过测试的库来构建你接下来的 API，请尝试使用 Pyramid。你不会失望的。

---

via: <https://opensource.com/article/20/1/python-web-api-pyramid-cornice>

作者：[Moshe Zadka](https://opensource.com/users/moshez) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


为你的 Python 平台类游戏添加跳跃功能

>
> 在本期使用 Python Pygame 模块编写视频游戏中，学会如何使用跳跃来对抗重力。
>
>
>

![](/data/attachment/album/202001/16/214917c8mxn82fot82fx88.jpg)

在本系列的 [前一篇文章](/article-11780-1.html) 中，你已经模拟了重力。但现在，你需要赋予你的角色跳跃的能力来对抗重力。

跳跃是对重力作用的暂时延缓。在这一小段时间里，你是向*上*跳，而不是被重力拉着向下落。但你一旦到达了跳跃的最高点，重力就会重新发挥作用，将你拉回地面。

在代码中，这种变化被表示为变量。首先，你需要为玩家精灵建立一个变量，使得 Python 能够跟踪该精灵是否正在跳跃中。一旦玩家精灵开始跳跃，他就会再次受到重力的作用，并被拉回最近的物体。

### 设置跳跃状态变量

你需要为你的 `Player` 类添加两个新变量：

* 一个是为了跟踪你的角色是否正在跳跃中，可通过你的玩家精灵是否站在坚实的地面来确定
* 一个是为了将玩家带回地面

将如下两个变量添加到你的 `Player` 类中。在下方的代码中，注释前的部分用于提示上下文，因此只需要添加最后两行：

```
                self.movex = 0
                self.movey = 0
                self.frame = 0
                self.health = 10
                # 此处是重力相关变量
                self.collide_delta = 0
                self.jump_delta = 6
```

第一个变量 `collide_delta` 被设为 0 是因为在正常状态下，玩家精灵没有处在跳跃中的状态。另一个变量 `jump_delta` 被设为 6，是为了防止精灵在第一次进入游戏世界时就发生反弹（实际上就是跳跃）。当你完成了本篇文章的示例，尝试把该变量设为 0 看看会发生什么。

### 跳跃中的碰撞

如果你是跳到一个蹦床上，那你的跳跃一定非常优美。但是如果你是跳向一面墙会发生什么呢？（千万不要去尝试！）不管你的起跳多么令人印象深刻，当你撞到比你更大更硬的物体时，你都会立马停下。（LCTT 译注：原理参考动量守恒定律）

为了在你的视频游戏中模拟这一点，你需要在你的玩家精灵与地面等东西发生碰撞时，将 `self.collide_delta` 变量设为 0。如果你的 `self.collide_delta` 不是 0 而是其它的什么值，那么你的玩家就会发生跳跃，并且当你的玩家与墙或者地面发生碰撞时无法跳跃。

在你的 `Player` 类的 `update` 方法中，将地面碰撞相关代码块修改为如下所示：

```
        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.y = worldy-ty-ty
            self.collide_delta = 0 # 停止跳跃
            if self.rect.y > g.rect.y:
                self.health -=1
                print(self.health)
```

这段代码块检查了地面精灵和玩家精灵之间发生的碰撞。当发生碰撞时，它会将玩家 Y 方向的坐标值设置为游戏窗口的高度减去一个瓷砖的高度再减去另一个瓷砖的高度。以此保证了玩家精灵是站在地面*上*，而不是嵌在地面里。同时它也将 `self.collide_delta` 设为 0，使得程序能够知道玩家未处在跳跃中。除此之外，它将 `self.movey` 设为 0，使得程序能够知道玩家当前未受到重力的牵引作用（这是游戏物理引擎的奇怪之处，一旦玩家落地，也就没有必要继续将玩家拉向地面）。

此处 `if` 语句用来检测玩家是否已经落到地面之*下*，如果是，那就扣除一点生命值作为惩罚。此处假定了你希望当你的玩家落到地图之外时失去生命值。这个设定不是必需的，它只是平台类游戏的一种惯例。更有可能的是，你希望这个事件能够触发另一些事件，或者说是一种能够让你的现实世界玩家沉迷于让精灵掉到屏幕之外的东西。一种简单的恢复方式是在玩家精灵掉落到地图之外时，将 `self.rect.y` 重新设置为 0，这样它就会在地图上方重新生成，并落到坚实的地面上。

### 撞向地面

模拟的重力使你玩家的 Y 坐标不断增大（LCTT 译注：此处原文中为 0，但在 Pygame 中越靠下方 Y 坐标应越大）。要实现跳跃，完成如下代码使你的玩家精灵离开地面，飞向空中。

在你的 `Player` 类的 `update` 方法中，添加如下代码来暂时延缓重力的作用：

```
        if self.collide_delta < 6 and self.jump_delta < 6:
            self.jump_delta = 6*2
            self.movey -= 33  # 跳跃的高度
            self.collide_delta += 6
            self.jump_delta    += 6
```

根据此代码所示，跳跃使玩家精灵向空中移动了 33 个像素。此处是*负* 33 是因为在 Pygame 中，越小的数代表距离屏幕顶端越近。

不过此事件视条件而定，只有当 `self.collide_delta` 小于 6（缺省值定义在你 `Player` 类的 `init` 方法中）并且 `self.jump_delta` 也于 6 的时候才会发生。此条件能够保证直到玩家碰到一个平台，才能触发另一次跳跃。换言之，它能够阻止空中二段跳。

在某些特殊条件下，你可能不想阻止空中二段跳，或者说你允许玩家进行空中二段跳。举个栗子，如果玩家获得了某个战利品，那么在他被敌人攻击到之前，都能够拥有空中二段跳的能力。

当你完成本篇文章中的示例，尝试将 `self.collide_delta` 和 `self.jump_delta` 设置为 0，从而获得百分之百的几率触发空中二段跳。

### 在平台上着陆

目前你已经定义了在玩家精灵摔落地面时的抵抗重力条件，但此时你的游戏代码仍保持平台与地面置于不同的列表中（就像本文中做的很多其他选择一样，这个设定并不是必需的，你可以尝试将地面作为另一种平台）。为了允许玩家精灵站在平台之上，你必须像检测地面碰撞一样，检测玩家精灵与平台精灵之间的碰撞。将如下代码放于你的 `update` 方法中：

```
        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.collide_delta = 0 # 跳跃结束
            self.movey = 0
```

但此处还有一点需要考虑：平台悬在空中，也就意味着玩家可以通过从上面或者从下面接触平台来与之互动。

确定平台如何与玩家互动取决于你，阻止玩家从下方到达平台也并不稀奇。将如下代码加到上方的代码块中，使得平台表现得像天花板或者说是藤架。只有在玩家精灵跳得比平台上沿更高时才能跳到平台上，但会阻止玩家从平台下方跳上来：

```
            if self.rect.y > p.rect.y:
                self.rect.y = p.rect.y+ty
            else:
                self.rect.y = p.rect.y-ty
```

此处 `if` 语句代码块的第一个子句阻止玩家精灵从平台正下方跳到平台上。如果它检测到玩家精灵的坐标比平台更大（在 Pygame 中，坐标更大意味着在屏幕的更下方），那么将玩家精灵新的 Y 坐标设置为当前平台的 Y 坐标加上一个瓷砖的高度。实际效果就是保证玩家精灵距离平台一个瓷砖的高度，防止其从下方穿过平台。

`else` 子句做了相反的事情。当程序运行到此处时，如果玩家精灵的 Y 坐标*不*比平台的更大，意味着玩家精灵是从空中落下（不论是由于玩家刚刚从此处生成，或者是玩家执行了跳跃）。在这种情况下，玩家精灵的 Y 坐标被设为平台的 Y 坐标减去一个瓷砖的高度（切记，在 Pygame 中更小的 Y 坐标代表在屏幕上的更高处）。这样就能保证玩家在平台*上*，除非他从平台上跳下来或者走下来。

你也可以尝试其他的方式来处理玩家与平台之间的互动。举个栗子，也许玩家精灵被设定为处在平台的“前面”，他能够无障碍地跳跃穿过平台并站在上面。或者你可以设计一种平台会减缓而又不完全阻止玩家的跳跃过程。甚至你可以通过将不同平台分到不同列表中来混合搭配使用。

### 触发一次跳跃

目前为此，你的代码已经模拟了所有必需的跳跃条件，但仍缺少一个跳跃触发器。你的玩家精灵的 `self.jump_delta` 初始值被设置为 6，只有当它比 6 小的时候才会触发更新跳跃的代码。

为跳跃变量设置一个新的设置方法，在你的 `Player` 类中创建一个 `jump` 方法，并将 `self.jump_delta` 设为小于 6 的值。通过使玩家精灵向空中移动 33 个像素，来暂时减缓重力的作用。

```
    def jump(self,platform_list):
        self.jump_delta = 0
```

不管你相信与否，这就是 `jump` 方法的全部。剩余的部分在 `update` 方法中，你已经在前面实现了相关代码。

要使你游戏中的跳跃功能生效，还有最后一件事情要做。如果你想不起来是什么，运行游戏并观察跳跃是如何生效的。

问题就在于你的主循环中没有调用 `jump` 方法。先前你已经为该方法创建了一个按键占位符，现在，跳跃键所做的就是将 `jump` 打印到终端。

### 调用 jump 方法

在你的主循环中，将*上*方向键的效果从打印一条调试语句，改为调用 `jump` 方法。

注意此处，与 `update` 方法类似，`jump` 方法也需要检测碰撞，因此你需要告诉它使用哪个 `plat_list`。

```
            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump(plat_list)
```

如果你倾向于使用空格键作为跳跃键，使用 `pygame.K_SPACE` 替代 `pygame.K_UP` 作为按键。另一种选择，你可以同时使用两种方式（使用单独的 `if` 语句），给玩家多一种选择。

现在来尝试你的游戏吧！在下一篇文章中，你将让你的游戏卷动起来。

![Pygame 平台类游戏](/data/attachment/album/202001/16/215002mj1tng0t1rhr3rte.jpg "Pygame platformer")

以下是目前为止的所有代码：

```
#!/usr/bin/env python3
# draw a world
# add a player and player control
# add player movement
# add enemy and basic collision
# add platform
# add gravity
# add jumping

# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

import pygame
import sys
import os

'''
Objects
'''

class Platform(pygame.sprite.Sprite):
    # x 坐标，y 坐标，图像宽度，图像高度，图像文件
    def __init__(self,xloc,yloc,imgw,imgh,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img)).convert()
        self.image.convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

class Player(pygame.sprite.Sprite):
    '''
    生成一个玩家
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.collide_delta = 0
        self.jump_delta = 6
        self.score = 1
        self.images = []
        for i in range(1,9):
            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect  = self.image.get_rect()

    def jump(self,platform_list):
        self.jump_delta = 0

    def gravity(self):
        self.movey += 3.2 # how fast player falls

        if self.rect.y > worldy and self.movey >= 0:
            self.movey = 0
            self.rect.y = worldy-ty

    def control(self,x,y):
        '''
        控制玩家移动
        '''
        self.movex += x
        self.movey += y

    def update(self):
        '''
        更新精灵位置
        '''

        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # 向左移动
        if self.movex < 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[self.frame//ani]

        # 向右移动
        if self.movex > 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[(self.frame//ani)+4]

        # 碰撞
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in enemy_hit_list:
            self.health -= 1
            #print(self.health)

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.collide_delta = 0 # stop jumping
            self.movey = 0
            if self.rect.y > p.rect.y:
                self.rect.y = p.rect.y+ty
            else:
                self.rect.y = p.rect.y-ty

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.y = worldy-ty-ty
            self.collide_delta = 0 # stop jumping
            if self.rect.y > g.rect.y:
                self.health -=1
                print(self.health)

        if self.collide_delta < 6 and self.jump_delta < 6:
            self.jump_delta = 6*2
            self.movey -= 33  # how high to jump
            self.collide_delta += 6
            self.jump_delta    += 6

class Enemy(pygame.sprite.Sprite):
    '''
    生成一个敌人
    '''
    def __init__(self,x,y,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img))
        self.movey = 0
        #self.image.convert_alpha()
        #self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        '''
        敌人移动
        '''
        distance = 80
        speed = 8

        self.movey += 3.2

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1

        if not self.rect.y >= worldy-ty-ty:
            self.rect.y += self.movey

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.movey = 0
            if self.rect.y > p.rect.y:
                self.rect.y = p.rect.y+ty
            else:
                self.rect.y = p.rect.y-ty

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.rect.y = worldy-ty-ty

class Level():
    def bad(lvl,eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0],eloc[1],'yeti.png') # 生成敌人
            enemy_list = pygame.sprite.Group() # 创建敌人组
            enemy_list.add(enemy)              # 将敌人添加到敌人组

        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list

    def loot(lvl,lloc):
        print(lvl)

    def ground(lvl,gloc,tx,ty):
        ground_list = pygame.sprite.Group()
        i=0
        if lvl == 1:
            while i < len(gloc):
                ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png')
                ground_list.add(ground)
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return ground_list

    def platform(lvl,tx,ty):
        plat_list = pygame.sprite.Group()
        ploc = []
        i=0
        if lvl == 1:
            ploc.append((0,worldy-ty-128,3))
            ploc.append((300,worldy-ty-256,3))
            ploc.append((500,worldy-ty-128,4))

            while i < len(ploc):
                j=0
                while j <= ploc[i][2]:
                    plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'ground.png')
                    plat_list.add(plat)
                    j=j+1
                print('run' + str(i) + str(ploc[i]))
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return plat_list

'''
Setup
'''
worldx = 960
worldy = 720

fps = 40 # 帧率
ani = 4  # 动画循环
clock = pygame.time.Clock()
pygame.init()
main = True

BLUE  = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)
ALPHA = (0,255,0)

world = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = world.get_rect()
player = Player() # 生成玩家
player.rect.x = 0
player.rect.y = 0
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10 # how fast to move
jump = -24

eloc = []
eloc = [200,20]
gloc = []
#gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630]
tx = 64 # 瓷砖尺寸
ty = 64 # 瓷砖尺寸

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1

enemy_list = Level.bad( 1, eloc )
ground_list = Level.ground( 1,gloc,tx,ty )
plat_list = Level.platform( 1,tx,ty )

'''
主循环
'''
while main == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
            main = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                print("LEFT")
                player.control(-steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                print("RIGHT")
                player.control(steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump(plat_list)

            if event.key == ord('q'):
                pygame.quit()
                sys.exit()
                main = False

#    world.fill(BLACK)
    world.blit(backdrop, backdropbox)
    player.gravity() # 检查重力
    player.update()
    player_list.draw(world) # 刷新玩家位置
    enemy_list.draw(world)  # 刷新敌人
    ground_list.draw(world)  # 刷新地面
    plat_list.draw(world)   # 刷新平台
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)
```

本期是使用 [Pygame](https://www.pygame.org/) 模块在 [Python 3](https://www.python.org/) 中创建视频游戏连载系列的第 7 期。往期文章为：

* [通过构建一个简单的掷骰子游戏去学习怎么用 Python 编程](/article-9071-1.html)
* [使用 Python 和 Pygame 模块构建一个游戏框架](/article-10850-1.html)
* [如何在你的 Python 游戏中添加一个玩家](/article-10858-1.html)
* [用 Pygame 使你的游戏角色移动起来](/article-10874-1.html)
* [如何向你的 Python 游戏中添加一个敌人](/article-10883-1.html)
* [在 Pygame 游戏中放置平台](/article-10902-1.html)
* [在你的 Python 游戏中模拟引力](/article-11780-1.html)

---

via: <https://opensource.com/article/19/12/jumping-python-platformer-game>

作者：[Seth Kenlon](https://opensource.com/users/seth) 选题：[lujun9972](https://github.com/lujun9972) 译者：[cycoe](https://github.com/cycoe) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


为什么你的发行版仍然在使用“过时的”Linux 内核？

Summary: 检查一下你的系统所使用的 Linux 内核版本，你十有八九会发现，按照 Linux 内核官网提供的信息，该内核版本已经达到使用寿命终期（EOL）了。

应该会有相应的工具来方便的使用最新的稳定内核版本吧

![](/data/attachment/album/202001/16/225806jbqyacu3loolobae.png)

[检查一下你的系统所使用的 Linux 内核版本](https://itsfoss.com/find-which-kernel-version-is-running-in-ubuntu/)，你十有八九会发现，按照 Linux 内核官网提供的信息，该内核版本已经达到使用寿命终期（EOL）了。

一个软件一旦达到了使用寿命终期，那么就意味着它再也不会得到 bug 修复和维护了。

这自然会引发一连串问题：为什么我的 Linux 发行版会使用一个已经达到使用寿命终期的内核呢？这样做有没有安全风险？我的系统还安全吗？

下面将逐一解答这些问题。

>
> **总结**
>
>
> 上游内核维护与你的发行版的内核维护是两个不同的概念。
>
>
> 例如，根据 Linux 内核官网，Linux 内核 4.15 版本可能已经达到使用寿命终期了，但是在 2023 年 4 月之前，Ubuntu 18.04 长期维护版本将会继续使用这个版本，并通过向后移植安全补丁和修复 bug 来提供维护。
>
>
>

### 检查 Linux 内核版本，以及是否达到使用寿命终期

首先，查看你的系统所使用的 Linux 内核版本：

```
uname -r
```

我使用的是 Ubuntu 18.04，输出的 Linux 内核版本如下：

```
abhishek@itsfoss:~$ uname -r
5.0.0-37-generic
```

接下来，可以到 [Linux 内核官网](https://www.kernel.org/)上看看哪些 Linux 内核版本仍然在维护状态。在网站主页上就可以看到相关信息。

你看到的内核版本状态应该类似于下图：

![Linux 内核状态](/data/attachment/album/202001/16/225934mpeb95byeky59spp.jpg)

如果你的内核版本没有列在内核官网主页上，就说明该版本已经达到了使用寿命终期。

可以看到，5.0 内核版本并不在列，这说明该内核版本已经不再得到维护。事实上，该版本在 [2019 年 6 月就已经达到使用寿命终期了](http://lkml.iu.edu/hypermail/linux/kernel/1906.0/02354.html)。

不幸的是，Linux 内核的生命周期没有任何规律可循。不是说常规的内核稳定发布版可以得到 X 月的维护、长期维护版本（LTS）可以得到 Y 年的维护。没有这回事。

根据实际需求，可能会存在内核的多个 LTS 版本，其使用寿命终期各不相同。在[这个页面](https://www.kernel.org/category/releases.html)上可以查到这些 LTS 版本的相关信息，包括计划的使用寿命终期。

那么问题来了：既然 Linux 内核官网上明确表示 5.0 版本的内核已经达到了使用寿命终期，Ubuntu 为什么还在提供这个内核版本呢？

### 你的发行版使用的 Linux 内核已经达到了使用寿命终期，但是没关系！

![](/data/attachment/album/202001/16/225922r6k6jggrj6kj2z8u.png)

你是否想过，为什么 Ubuntu/Debian/Fedora 等发行版被称为 Linux “发行版”？这是因为，它们“发行” Linux 内核。

这些发行版会对 Linux 内核进行不同的修改，并添加各种 GUI 元素（包括桌面环境、显示服务器等）以及软件，然后再呈现给用户。

按照通常的工作流，Linux 发行版会选择一个内核，提供给其用户，然后在接下来的几个月、几年中，甚至是达到内核的使用寿命终期之后，仍然会继续使用该内核。

这样能够保障安全吗？其实是可以的，因为 **发行版会通过向后移植全部的重要修补来维护内核**。

换句话说，你的 Linux 发行版会确保 Linux 内核没有漏洞和 bug，并且已经通过向后移植获得了重要的新特性。在“过时的旧版本 Linux 内核”上，其实有着数以千计的改动。

如果 Linux 内核网站上说某个内核版本已经达到了使用寿命终期，那么就意味着 Linux 内核的核心维护团队不会再对该内核版本进行升级和打补丁了。

但与此同时，Debian/Ubuntu 或者其他发行版的开发者们会继续工作，通过从（由内核核心团队维护的）更新的内核版本中迁移相关的修改，维持这个老版本的生命力。

重点在于，即使你的发行版看上去是在使用一个已经过时的 Linux 内核，其实该内核也得到了良好的维护，并非是真的过时了。

### 你是否应该使用最新的稳定内核版本？

![](/data/attachment/album/202001/16/225924xh37qhvppvk333h7.png)

新的 Linux 内核稳定版本每隔 2 到 3 个月发布一次，有不少用户跃跃欲试。

实话说，除非有十分充分的理由，否则不应该使用最新版本的稳定内核。你使用的发行版并不会提供这个选项，你也不能指望通过在键盘上敲出 `sudo apt give-me-the-latest-stable-kernel` 解决问题。

此外，手动[安装主流 Linux 内核版本](https://wiki.ubuntu.com/Kernel/MainlineBuilds)本身就是一个挑战。即使安装成功，之后每次发布 bug 修复的时候，负责更新内核的就会是你了。此外，当新内核达到使用寿命终期之后，你就有责任将它升级到更新的内核版本了。和常规的 [Ubuntu 更新](https://itsfoss.com/update-ubuntu/)不同，内核升级无法通过 `apt upgrade` 完成。

同样需要记住的是，切换到主流内核之后，可能就无法使用你的发行版提供的一些驱动程序和补丁了。

正如 [Greg Kroah-Hartman](https://en.wikipedia.org/wiki/Greg_Kroah-Hartman)所言，“**你能使用的最好的内核，就是别人在维护的内核。**”除了你的 Linux 发行版之外，又有谁更胜任这份工作呢！

希望你对这个主题已经有了更好的理解。下回发现你的系统正在使用的内核版本已经达到使用寿命终期的时候，希望你不会感到惊慌失措。

欢迎在下面的评论区中留下你的疑问或建议。

---

via: <https://itsfoss.com/why-distros-use-old-kernel/>

作者：[Abhishek Prakash](https://itsfoss.com/author/abhishek/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[chen-ni](https://github.com/chen-ni) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 Syncthing 在多个设备间同步文件

Syncthing item is not in UTF8 encoding . syncthing(v1.4.0)不支持中文的文件和文件夹

>
> 2020 年，在我们的 20 个使用开源提升生产力的系列文章中，首先了解如何使用 Syncthing 同步文件。
>
>
>

![](/data/attachment/album/202001/18/123416rebvs7sjwm6c889y.jpg)

去年，我在 19 天里给你介绍了 19 个新（对你而言）的生产力工具。今年，我换了一种方式：使用你在使用或者还没使用的工具，构建一个使你可以在新一年更加高效的环境。

### 使用 Synthing 同步文件

设置新机器很麻烦。我们都有在机器之间复制的“标准设置”。多年来，我使用了很多方法来使它们在计算机之间同步。在过去（这会告诉你我年纪有多大了），曾经是软盘、然后是 Zip 磁盘、U 盘、SCP、Rsync、Dropbox、ownCloud，你想到的都试过。但这些似乎对我都不够好。

然后我偶然发现了 [Syncthing](https://syncthing.net/)。

![syncthing console](/data/attachment/album/202001/18/123432snnac64vn555rcng.png "syncthing console")

Syncthing 是一个轻量级的点对点文件同步系统。你不需要为服务付费，也不需要第三方服务器，而且速度很快。以我的经验，比文件同步中的许多“大牌”要快得多。

Syncthing 可在 Linux、MacOS、Windows 和多种 BSD 中使用。还有一个 Android 应用（但尚无官方 iOS 版本）。以上所有终端都有方便的图形化前端（尽管我不会在这里介绍）。在 Linux 上，大多数发行版都有可用的软件包，因此安装非常简单。

![Installing Syncthing on Ubuntu](/data/attachment/album/202001/18/123441csudwrfs5rksro5r.png "Installing Syncthing on Ubuntu")

首次启动 Syncthing 时，它将启动 Web 浏览器以配置守护程序。第一台计算机上没有太多要做，但是这是一个很好的机会来介绍一下用户界面 （UI）。最重要的是在右上方的 “Actions” 菜单下的 “System ID”。

![Machine ID](/data/attachment/album/202001/18/123442zw0h0ipe0h8i38wg.png "Machine ID")

设置第一台计算机后，请在第二台计算机上重复安装。在 UI 中，右下方将显示一个按钮，名为 “Add Remote Device”。单击该按钮，你将会看到一个要求输入 “Device ID and a Name” 的框。从第一台计算机上复制并粘贴 “Device ID”，然后单击 “Save”。

你应该会在第一台上看到一个请求添加第二台的弹出窗口。接受后，新机器将显示在第一台机器的右下角。与第二台计算机共享默认目录。单击 “Default Folder”，然后单击 “Edit” 按钮。弹出窗口的顶部有四个链接。单击 “Sharing”，然后选择第二台计算机。单击 “Save”，然后查看第二台计算机。你会看到一个接受共享目录的提示。接受后，它将开始在两台计算机之间同步文件。

![Sharing a directory in Syncthing](/data/attachment/album/202001/18/123443qzsqg4n9w93c0cl6.png "Sharing a directory in Syncthing")

测试从一台计算机上复制文件到默认目录（“/你的家目录/Share”）。它应该很快会在另一台上出现。

你可以根据需要添加任意数量的目录，这非常方便。如你在第一张图中所看到的，我有一个用于保存配置的 `myconfigs` 文件夹。当我买了一台新机器时，我只需安装 Syncthing，如果我在一台机器上调整了配置，我不必更新所有，它会自动更新。

---

via: <https://opensource.com/article/20/1/sync-files-syncthing>

作者：[Kevin Sonney](https://opensource.com/users/ksonney) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 Git 来管理 Git 服务器

有没有gitolite管理方面的研究尝试，比如当前有哪些用户在拉去代码，拉去的是什么代码，拉去开始和结束时间的统计。。。

>
> 借助 Gitolite，你可以使用 Git 来管理 Git 服务器。在我们的系列文章中了解这些鲜为人知的 Git 用途。
>
>
>

![](/data/attachment/album/202001/18/132045yrr1pb9n497tfbiy.png)

正如我在系列文章中演示的那样，[Git](https://git-scm.com/) 除了跟踪源代码外，还可以做很多事情。信不信由你，Git 甚至可以管理你的 Git 服务器，因此你可以或多或少地使用 Git 本身来运行 Git 服务器。

当然，这涉及除日常使用 Git 之外的许多组件，其中最重要的是 [Gitolite](http://gitolite.com)，该后端应用程序可以管理你使用 Git 的每个细微的配置。Gitolite 的优点在于，由于它使用 Git 作为其前端接口，因此很容易将 Git 服务器管理集成到其他基于 Git 的工作流中。Gitolite 可以精确控制谁可以访问你服务器上的特定存储库以及他们具有哪些权限。你可以使用常规的 Linux 系统工具自行管理此类事务，但是如果有好几个用户和不止一两个仓库，则需要大量的工作。

Gitolite 的开发人员做了艰苦的工作，使你可以轻松地为许多用户提供对你的 Git 服务器的访问权，而又不让他们访问你的整个环境 —— 而这一切，你可以使用 Git 来完成全部工作。

Gitolite 并**不是**图形化的管理员和用户面板。优秀的 [Gitea](http://gitea.io) 项目可提供这种体验，但是本文重点介绍 Gitolite 的简单优雅和令人舒适的熟悉感。

### 安装 Gitolite

假设你的 Git 服务器运行在 Linux 上，则可以使用包管理器安装 Gitolite（在 CentOS 和 RHEL 上为 `yum`，在 Debian 和 Ubuntu 上为 `apt`，在 OpenSUSE 上为 `zypper` 等）。例如，在 RHEL 上：

```
$ sudo yum install gitolite3
```

许多发行版的存储库提供的仍是旧版本的 Gitolite，但最新版本为版本 3。

你必须具有对服务器的无密码 SSH 访问权限。如果愿意，你可以使用密码登录服务器，但是 Gitolite 依赖于 SSH 密钥，因此必须配置使用密钥登录的选项。如果你不知道如何配置服务器以进行无密码 SSH 访问，请首先学习如何进行操作（Steve Ovens 的 Ansible 文章的[设置 SSH 密钥身份验证](Setting%20up%20SSH%20key%20authentication)部分对此进行了很好的说明）。这是加强服务器管理的安全以及运行 Gitolite 的重要组成部分。

### 配置 Git 用户

如果没有 Gitolite，则如果某人请求访问你在服务器上托管的 Git 存储库时，则必须向该人提供用户帐户。Git 提供了一个特殊的外壳，即 `git-shell`，这是一个仅执行 Git 任务的特别的特定 shell。这可以让你有个只能通过非常受限的 Shell 环境来过滤访问你的服务器的用户。

这个解决方案是一个办法，但通常意味着用户可以访问服务器上的所有存储库，除非你具有用于组权限的良好模式，并在创建新存储库时严格遵循这些权限。这种方式还需要在系统级别进行大量手动配置，这通常是只有特定级别的系统管理员才能做的工作，而不一定是通常负责 Git 存储库的人员。

Gitolite 通过为需要访问任何存储库的每个人指定一个用户名来完全回避此问题。默认情况下，该用户名是 `git`，并且由于 Gitolite 的文档中假定使用的是它，因此在学习该工具时保留它是一个很好的默认设置。对于曾经使用过 GitLab 或 GitHub 或任何其他 Git 托管服务的人来说，这也是一个众所周知的约定。

Gitolite 将此用户称为**托管用户**。在服务器上创建一个帐户以充当托管用户（我习惯使用 `git`，因为这是惯例）：

```
$ sudo adduser --create-home git
```

为了控制该 `git` 用户帐户，该帐户必须具有属于你的有效 SSH 公钥。你应该已经进行了设置，因此复制你的公钥（**而不是你的私钥**）添加到 `git` 用户的家目录中：

```
$ sudo cp ~/.ssh/id_ed25519.pub /home/git/
$ sudo chown git:git /home/git/id_ed25519.pub
```

如果你的公钥不以扩展名 `.pub` 结尾，则 Gitolite 不会使用它，因此请相应地重命名该文件。切换为该用户帐户以运行 Gitolite 的安装程序：

```
$ sudo su - git
$ gitolite setup --pubkey id_ed25519.pub
```

安装脚本运行后，`git` 的家用户目录将有一个 `repository` 目录，该目录（目前）包含存储库 `git-admin.git` 和 `testing.git`。这就是该服务器所需的全部设置，现在请登出 `git` 用户。

### 使用 Gitolite

管理 Gitolite 就是编辑 Git 存储库中的文本文件，尤其是 `gitolite-admin.git` 中的。你不会通过 SSH 进入服务器来进行 Git 管理，并且 Gitolite 也建议你不要这样尝试。在 Gitolite 服务器上存储你和你的用户的存储库是个**裸**存储库，因此最好不要使用它们。

```
$ git clone git@example.com:gitolite-admin.git gitolite-admin.git
$ cd gitolite-admin.git
$ ls -1
conf
keydir
```

该存储库中的 `conf` 目录包含一个名为 `gitolite.conf` 的文件。在文本编辑器中打开它，或使用 `cat` 查看其内容：

```
repo gitolite-admin
    RW+     =   id_ed22519

repo testing
    RW+     =   @all
```

你可能对该配置文件的功能有所了解：`gitolite-admin` 代表此存储库，并且 `id_ed25519` 密钥的所有者具有读取、写入和管理 Git 的权限。换句话说，不是将用户映射到普通的本地 Unix 用户（因为所有用户都使用 `git` 用户托管用户身份），而是将用户映射到 `keydir` 目录中列出的 SSH 密钥。

`testing.git` 存储库使用特殊组符号为访问服务器的每个人提供了全部权限。

#### 添加用户

如果要向 Git 服务器添加一个名为 `alice` 的用户，Alice 必须向你发送她的 SSH 公钥。Gitolite 使用文件名的 `.pub` 扩展名左边的任何内容作为该 Git 用户的标识符。不要使用默认的密钥名称值，而是给密钥指定一个指示密钥所有者的名称。如果用户有多个密钥（例如，一个用于笔记本电脑，一个用于台式机），则可以使用子目录来避免文件名冲突。例如，Alice 在笔记本电脑上使用的密钥可能是默认的 `id_rsa.pub`，因此将其重命名为`alice.pub` 或类似名称（或让用户根据其计算机上的本地用户帐户来命名密钥），然后将其放入 `gitolite-admin.git/keydir/work/laptop/` 目录中。如果她从她的桌面计算机发送了另一个密钥，命名为 `alice.pub`（与上一个相同），然后将其添加到 `keydir/home/desktop/` 中。另一个密钥可能放到 `keydir/home/desktop/` 中，依此类推。Gitolite 递归地在 `keydir` 中搜索与存储库“用户”相匹配的 `.pub` 文件，并将所有匹配项视为相同的身份。

当你将密钥添加到 `keydir` 目录时，必须将它们提交回服务器。这是一件很容易忘记的事情，这里有一个使用自动化的 Git 应用程序（例如 [Sparkleshare](https://opensource.com/article/19/4/file-sharing-git)）的真正的理由，因此任何更改都将立即提交给你的 Gitolite 管理员。第一次忘记提交和推送，在浪费了三个小时的你和你的用户的故障排除时间之后，你会发现 Gitolite 是使用 Sparkleshare 的完美理由。

```
$ git add keydir
$ git commit -m 'added alice-laptop-0.pub'
$ git push origin HEAD
```

默认情况下，Alice 可以访问 `testing.git` 目录，因此她可以使用该目录测试连接性和功能。

#### 设置权限

与用户一样，目录权限和组也是从你可能习惯的的常规 Unix 工具中抽象出来的（或可从在线信息查找）。在 `gitolite-admin.git/conf` 目录中的 `gitolite.conf` 文件中授予对项目的权限。权限分为四个级别：

* `R` 允许只读。在存储库上具有 `R` 权限的用户可以克隆它，仅此而已。
* `RW` 允许用户执行分支的快进推送、创建新分支和创建新标签。对于大多数用户来说，这个基本上就像是一个“普通”的 Git 存储库。
* `RW+` 允许可能具有破坏性的 Git 动作。用户可以执行常规的快进推送、回滚推送、变基以及删除分支和标签。你可能想要或不希望将其授予项目中的所有贡献者。
* `-` 明确拒绝访问存储库。这与未在存储库的配置中列出的用户相同。

通过调整 `gitolite.conf` 来创建一个新的存储库或修改现有存储库的权限。例如，授予 Alice 权限来管理一个名为 `widgets.git` 的新存储库：

```
repo gitolite-admin
    RW+     =   id_ed22519

repo testing
    RW+     =   @all

repo widgets
    RW+     =   alice
```

现在，Alice（也仅有 Alice 一个人）可以克隆该存储库：

```
[alice]$ git clone git@example.com:widgets.git
Cloning into 'widgets'...
warning: You appear to have cloned an empty repository.
```

在第一次推送时，Alice 必须使用 `-u` 选项将其分支发送到空存储库（如同她在任何 Git 主机上做的一样）。

为了简化用户管理，你可以定义存储库组：

```
@qtrepo = widgets
@qtrepo = games

repo gitolite-admin
    RW+     =   id_ed22519

repo testing
    RW+     =   @all

repo @qtrepo
    RW+     =   alice
```

正如你可以创建组存储库一样，你也可以对用户进行分组。默认情况下存在一个用户组：`@all`。如你所料，它包括所有用户，无一例外。你也可以创建自己的组：

```
@qtrepo = widgets
@qtrepo = games

@developers = alice bob

repo gitolite-admin
    RW+     =   id_ed22519

repo testing
    RW+     =   @all

repo @qtrepo
    RW+     =   @developers
```

与添加或修改密钥文件一样，对 `gitolite.conf` 文件的任何更改都必须提交并推送以生效。

### 创建存储库

默认情况下，Gitolite 假设存储库的创建是从上至下进行。例如，有权访问 Git 服务器的项目经理创建了一个项目存储库，并通过 Gitolite 管理仓库添加了开发人员。

实际上，你可能更愿意向用户授予创建存储库的权限。Gitolite 称这些为“<ruby> 野生仓库（通配仓库） <rt>  wild repos </rt></ruby>”（我不确定这是关于仓库的形成方式的描述，还是指配置文件所需的通配符）。这是一个例子：

```
@managers = alice bob

repo foo/CREATOR/[a-z]..*
    C   =   @managers
    RW+ =   CREATOR
    RW  =   WRITERS
    R   =   READERS
```

第一行定义了一组用户：该组称为 `@managers`，其中包含用户 `alice` 和 `bob`。下一行设置了通配符允许创建尚不存在的存储库，放在名为 `foo` 的目录下的创建该存储库的用户名的子目录中。例如：

```
[alice]$ git clone git@example.com:foo/alice/cool-app.git
Cloning into cool-app'...
Initialized empty Git repository in /home/git/repositories/foo/alice/cool-app.git
warning: You appear to have cloned an empty repository.
```

野生仓库的创建者可以使用一些机制来定义谁可以读取和写入其存储库，但是他们是有范围限定的。在大多数情况下，Gitolite 假定由一组特定的用户来管理项目权限。一种解决方案是使用 Git 挂钩来授予所有用户对 `gitolite-admin` 的访问权限，以要求管理者批准将更改合并到 master 分支中。

### 了解更多

Gitolite 具有比此介绍性文章所涵盖的更多功能，因此请尝试一下。其[文档](http://gitolite.com/gitolite/quick_install.html)非常出色，一旦你通读了它，就可以自定义 Gitolite 服务器，以向用户提供你喜欢的任何级别的控制。Gitolite 是一种维护成本低、简单的系统，你可以安装、设置它，然后基本上就可以将其忘却。

---

via: <https://opensource.com/article/19/4/server-administration-git>

作者：[Seth Kenlon](https://opensource.com/users/seth/users/seth) 选题：[lujun9972](https://github.com/lujun9972) 译者：[wxy](https://github.com/wxy) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 Stow 管理多台机器配置

>
> 2020 年，在我们的 20 个使用开源提升生产力的系列文章中，让我们了解如何使用 Stow 跨机器管理配置。
>
>
>

![](/data/attachment/album/202001/18/141330jdcjalqzjal84a03.jpg)

去年，我在 19 天里给你介绍了 19 个新（对你而言）的生产力工具。今年，我换了一种方式：使用你在使用或者还没使用的工具，构建一个使你可以在新一年更加高效的环境。

### 使用 Stow 管理符号链接

昨天，我解释了如何使用 [Syncthing](https://syncthing.net/) 在多台计算机上保持文件同步。但是，这只是我用来保持配置一致性的工具之一。还有另一个表面上看起来更简单的工具：[Stow](https://www.gnu.org/software/stow/)。

![Stow help screen](/data/attachment/album/202001/18/141427lhn98rqyqq9h2d22.png "Stow help screen")

Stow 管理符号链接。默认情况下，它会链接目录到上一级目录。还有设置源和目标目录的选项，但我通常不使用它们。

正如我在 Syncthing 的[文章](/article-11793-1.html) 中提到的，我使用 Syncthing 来保持 `myconfigs` 目录在我所有的计算机上一致。`myconfigs` 目录下面有多个子目录。每个子目录包含我经常使用的应用之一的配置文件。

![myconfigs directory](/data/attachment/album/202001/18/141430ov0i4fv3iq84h322.png "myconfigs directory")

在每台计算机上，我进入 `myconfigs` 目录，并运行 `stow -S <目录名称>` 以将目录中的文件符号链接到我的家目录。例如，在 `vim` 目录下，我有 `.vimrc` 和 `.vim` 目录。在每台机器上，我运行 `stow -S vim` 来创建符号链接 `~/.vimrc` 和 `~/.vim`。当我在一台计算机上更改 Vim 配置时，它会应用到我的所有机器上。

然而，有时候，我需要一些特定于机器的配置，这就是为什么我有如 `msmtp-personal` 和 `msmtp-elastic`（我的雇主）这样的目录。由于我的 `msmtp` SMTP 客户端需要知道要中继电子邮件服务器，并且每个服务器都有不同的设置和凭据，我会使用 `-D` 标志来取消链接，接着链接另外一个。

![Unstow one, stow the other](/data/attachment/album/202001/18/141437u3g477ny77bikeki.png "Unstow one, stow the other")

有时我要给配置添加文件。为此，有一个 `-R` 选项来“重新链接”。例如，我喜欢在图形化 Vim 中使用一种与控制台不同的特定字体。除了标准 `.vimrc` 文件，`.gvimrc` 文件能让我设置特定于图形化版本的选项。当我第一次设置它时，我移动 `~/.gvimrc` 到 `~/myconfigs/vim` 中，然后运行 `stow -R vim`，它取消链接并重新链接该目录中的所有内容。

Stow 让我使用一个简单的命令行在多种配置之间切换，并且，结合 Syncthing，我可以确保无论我身在何处或在哪里进行更改，我都有我喜欢的工具的设置。

---

via: <https://opensource.com/article/20/1/configuration-management-stow>

作者：[Kevin Sonney](https://opensource.com/users/ksonney) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


6 个方便的 Git 脚本

Summary: 当使用 Git 存储库时，这六个 Bash 脚本将使你的生活更轻松。

当使用 Git 存储库时，这六个 Bash 脚本将使你的生活更轻松。

>
> 当使用 Git 存储库时，这六个 Bash 脚本将使你的生活更轻松。
>
>
>

![](/data/attachment/album/202001/18/231713jegbk8fyek798gxb.jpg)

我编写了许多 Bash 脚本，这些脚本使我在使用 Git 存储库时工作更加轻松。我的许多同事说没有必要：我所做的一切都可以用 Git 命令完成。虽然这可能是正确的，但我发现脚本远比尝试找出适当的 Git 命令来执行我想要的操作更加方便。

### 1、gitlog

`gitlog` 打印针对 master 分支的当前补丁的简短列表。它从最旧到最新打印它们，并显示作者和描述，其中 `H` 代表 `HEAD`，`^` 代表 `HEAD^`，`2` 代表 `HEAD~2`，依此类推。例如：

```
$ gitlog
-----------------------[ recovery25 ]-----------------------
(snip)
11 340d27a33895 Bob Peterson     gfs2: drain the ail2 list after io errors
10 9b3c4e6efb10 Bob Peterson     gfs2: clean up iopen glock mess in gfs2_create_inode
 9 d2e8c22be39b Bob Peterson     gfs2: Do proper error checking for go_sync family of glops
 8 9563e31f8bfd Christoph Hellwig gfs2: use page_offset in gfs2_page_mkwrite
 7 ebac7a38036c Christoph Hellwig gfs2: don't use buffer_heads in gfs2_allocate_page_backing
 6 f703a3c27874 Andreas Gruenbacher gfs2: Improve mmap write vs. punch_hole consistency
 5 a3e86d2ef30e Andreas Gruenbacher gfs2: Multi-block allocations in gfs2_page_mkwrite
 4 da3c604755b0 Andreas Gruenbacher gfs2: Fix end-of-file handling in gfs2_page_mkwrite
 3 4525c2f5b46f Bob Peterson     Rafael Aquini's slab instrumentation
 2 a06a5b7dea02 Bob Peterson     GFS2: Add go_get_holdtime to gl_ops
 ^ 8ba93c796d5c Bob Peterson     gfs2: introduce new function remaining_hold_time and use it in dq
 H e8b5ff851bb9 Bob Peterson     gfs2: Allow rgrps to have a minimum hold time
```

如果我想查看其他分支上有哪些补丁，可以指定一个替代分支：

```
$ gitlog recovery24
```

### 2、gitlog.id

`gitlog.id` 只是打印出补丁的 SHA1 ID：

```
$ gitlog.id
-----------------------[ recovery25 ]-----------------------
56908eeb6940 2ca4a6b628a1 fc64ad5d99fe 02031a00a251 f6f38da7dd18 d8546e8f0023 fc3cc1f98f6b 12c3e0cb3523 76cce178b134 6fc1dce3ab9c 1b681ab074ca 26fed8de719b 802ff51a5670 49f67a512d8c f04f20193bbb 5f6afe809d23 2030521dc70e dada79b3be94 9b19a1e08161 78a035041d3e f03da011cae2 0d2b2e068fcd 2449976aa133 57dfb5e12ccd 53abedfdcf72 6fbdda3474b3 49544a547188 187032f7a63c 6f75dae23d93 95fc2a261b00 ebfb14ded191 f653ee9e414a 0e2911cb8111 73968b76e2e3 8a3e4cb5e92c a5f2da803b5b 7c9ef68388ed 71ca19d0cba8 340d27a33895 9b3c4e6efb10 d2e8c22be39b 9563e31f8bfd ebac7a38036c f703a3c27874 a3e86d2ef30e da3c604755b0 4525c2f5b46f a06a5b7dea02 8ba93c796d5c e8b5ff851bb9
```

同样，它假定是当前分支，但是如果需要，我可以指定其他分支。

### 3、gitlog.id2

`gitlog.id2` 与 `gitlog.id` 相同，但顶部没有显示分支的行。这对于从一个分支挑选所有补丁到当前分支很方便：

```
$ # 创建一个新分支
$ git branch --track origin/master
$ # 检出刚刚创建的新分支
$ git checkout recovery26
$ # 从旧的分支挑选所有补丁到新分支
$ for i in `gitlog.id2 recovery25` ; do git cherry-pick $i ;done
```

### 4、gitlog.grep

`gitlog.grep` 会在该补丁集合中寻找一个字符串。例如，如果我发现一个错误并想修复引用了函数 `inode_go_sync` 的补丁，我可以简单地执行以下操作：

```
$ gitlog.grep inode_go_sync
-----------------------[ recovery25 - 50 patches ]-----------------------
(snip)
11 340d27a33895 Bob Peterson     gfs2: drain the ail2 list after io errors
10 9b3c4e6efb10 Bob Peterson     gfs2: clean up iopen glock mess in gfs2_create_inode
 9 d2e8c22be39b Bob Peterson     gfs2: Do proper error checking for go_sync family of glops
152:-static void inode_go_sync(struct gfs2_glock *gl)
153:+static int inode_go_sync(struct gfs2_glock *gl)
163:@@ -296,6 +302,7 @@ static void inode_go_sync(struct gfs2_glock *gl)
 8 9563e31f8bfd Christoph Hellwig gfs2: use page_offset in gfs2_page_mkwrite
 7 ebac7a38036c Christoph Hellwig gfs2: don't use buffer_heads in gfs2_allocate_page_backing
 6 f703a3c27874 Andreas Gruenbacher gfs2: Improve mmap write vs. punch_hole consistency
 5 a3e86d2ef30e Andreas Gruenbacher gfs2: Multi-block allocations in gfs2_page_mkwrite
 4 da3c604755b0 Andreas Gruenbacher gfs2: Fix end-of-file handling in gfs2_page_mkwrite
 3 4525c2f5b46f Bob Peterson     Rafael Aquini's slab instrumentation
 2 a06a5b7dea02 Bob Peterson     GFS2: Add go_get_holdtime to gl_ops
 ^ 8ba93c796d5c Bob Peterson     gfs2: introduce new function remaining_hold_time and use it in dq
 H e8b5ff851bb9 Bob Peterson     gfs2: Allow rgrps to have a minimum hold time
```

因此，现在我知道补丁 `HEAD~9` 是需要修复的补丁。我使用 `git rebase -i HEAD~10` 编辑补丁 9，`git commit -a --amend`，然后 `git rebase --continue` 以进行必要的调整。

### 5、gitbranchcmp3

`gitbranchcmp3` 使我可以将当前分支与另一个分支进行比较，因此我可以将较旧版本的补丁与我的较新版本进行比较，并快速查看已更改和未更改的内容。它生成一个比较脚本（使用了 KDE 工具 [Kompare](https://kde.org/applications/development/org.kde.kompare)，该工具也可在 GNOME3 上使用）以比较不太相同的补丁。如果除行号外没有其他差异，则打印 `[SAME]`。如果仅存在注释差异，则打印 `[same]`（小写）。例如：

```
$ gitbranchcmp3 recovery24
Branch recovery24 has 47 patches
Branch recovery25 has 50 patches

(snip)
38 87eb6901607a 340d27a33895 [same] gfs2: drain the ail2 list after io errors
39 90fefb577a26 9b3c4e6efb10 [same] gfs2: clean up iopen glock mess in gfs2_create_inode
40 ba3ae06b8b0e d2e8c22be39b [same] gfs2: Do proper error checking for go_sync family of glops
41 2ab662294329 9563e31f8bfd [SAME] gfs2: use page_offset in gfs2_page_mkwrite
42 0adc6d817b7a ebac7a38036c [SAME] gfs2: don't use buffer_heads in gfs2_allocate_page_backing
43 55ef1f8d0be8 f703a3c27874 [SAME] gfs2: Improve mmap write vs. punch_hole consistency
44 de57c2f72570 a3e86d2ef30e [SAME] gfs2: Multi-block allocations in gfs2_page_mkwrite
45 7c5305fbd68a da3c604755b0 [SAME] gfs2: Fix end-of-file handling in gfs2_page_mkwrite
46 162524005151 4525c2f5b46f [SAME] Rafael Aquini's slab instrumentation
47              a06a5b7dea02 [    ] GFS2: Add go_get_holdtime to gl_ops
48              8ba93c796d5c [    ] gfs2: introduce new function remaining_hold_time and use it in dq
49              e8b5ff851bb9 [    ] gfs2: Allow rgrps to have a minimum hold time

Missing from recovery25:
The missing:
Compare script generated at: /tmp/compare_mismatches.sh
```

### 6、gitlog.find

最后，我有一个 `gitlog.find` 脚本，可以帮助我识别补丁程序的上游版本在哪里以及每个补丁的当前状态。它通过匹配补丁说明来实现。它还会生成一个比较脚本（再次使用了 Kompare），以将当前补丁与上游对应补丁进行比较：

```
$ gitlog.find
-----------------------[ recovery25 - 50 patches ]-----------------------
(snip)
11 340d27a33895 Bob Peterson     gfs2: drain the ail2 list after io errors
lo 5bcb9be74b2a Bob Peterson     gfs2: drain the ail2 list after io errors
10 9b3c4e6efb10 Bob Peterson     gfs2: clean up iopen glock mess in gfs2_create_inode
fn 2c47c1be51fb Bob Peterson     gfs2: clean up iopen glock mess in gfs2_create_inode
 9 d2e8c22be39b Bob Peterson     gfs2: Do proper error checking for go_sync family of glops
lo feb7ea639472 Bob Peterson     gfs2: Do proper error checking for go_sync family of glops
 8 9563e31f8bfd Christoph Hellwig gfs2: use page_offset in gfs2_page_mkwrite
ms f3915f83e84c Christoph Hellwig gfs2: use page_offset in gfs2_page_mkwrite
 7 ebac7a38036c Christoph Hellwig gfs2: don't use buffer_heads in gfs2_allocate_page_backing
ms 35af80aef99b Christoph Hellwig gfs2: don't use buffer_heads in gfs2_allocate_page_backing
 6 f703a3c27874 Andreas Gruenbacher gfs2: Improve mmap write vs. punch_hole consistency
fn 39c3a948ecf6 Andreas Gruenbacher gfs2: Improve mmap write vs. punch_hole consistency
 5 a3e86d2ef30e Andreas Gruenbacher gfs2: Multi-block allocations in gfs2_page_mkwrite
fn f53056c43063 Andreas Gruenbacher gfs2: Multi-block allocations in gfs2_page_mkwrite
 4 da3c604755b0 Andreas Gruenbacher gfs2: Fix end-of-file handling in gfs2_page_mkwrite
fn 184b4e60853d Andreas Gruenbacher gfs2: Fix end-of-file handling in gfs2_page_mkwrite
 3 4525c2f5b46f Bob Peterson     Rafael Aquini's slab instrumentation
   Not found upstream
 2 a06a5b7dea02 Bob Peterson     GFS2: Add go_get_holdtime to gl_ops
   Not found upstream
 ^ 8ba93c796d5c Bob Peterson     gfs2: introduce new function remaining_hold_time and use it in dq
   Not found upstream
 H e8b5ff851bb9 Bob Peterson     gfs2: Allow rgrps to have a minimum hold time
   Not found upstream
Compare script generated: /tmp/compare_upstream.sh
```

补丁显示为两行，第一行是你当前的修补程序，然后是相应的上游补丁，以及 2 个字符的缩写，以指示其上游状态：

* `lo` 表示补丁仅在本地（`local`）上游 Git 存储库中（即尚未推送到上游）。
* `ms` 表示补丁位于 Linus Torvald 的主（`master`）分支中。
* `fn` 意味着补丁被推送到我的 “for-next” 开发分支，用于下一个上游合并窗口。 我的一些脚本根据我通常使用 Git 的方式做出假设。例如，当搜索上游补丁时，它使用我众所周知的 Git 树的位置。因此，你需要调整或改进它们以适合你的条件。`gitlog.find` 脚本旨在仅定位 [GFS2](https://en.wikipedia.org/wiki/GFS2) 和 [DLM](https://en.wikipedia.org/wiki/Distributed_lock_manager) 补丁，因此，除非你是 GFS2 开发人员，否则你需要针对你感兴趣的组件对其进行自定义。

### 源代码

以下是这些脚本的源代码。

#### 1、gitlog

```
#!/bin/bash
branch=$1

if test "x$branch" = x; then
    branch=`git branch -a | grep "*" | cut -d ' ' -f2`
fi

patches=0
tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`

LIST=`git log --reverse --abbrev-commit --pretty=oneline $tracking..$branch | cut -d ' ' -f1 |paste -s -d ' '`
for i in $LIST; do patches=$(echo $patches + 1 | bc);done

if [[ $branch =~ .*for-next.* ]]
then
    start=HEAD
#    start=origin/for-next
else
    start=origin/master
fi

tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`

/usr/bin/echo "-----------------------[" $branch "]-----------------------"
patches=$(echo $patches - 1 | bc);
for i in $LIST; do
    if [ $patches -eq 1 ]; then
        cnt=" ^"
    elif [ $patches -eq 0 ]; then
        cnt=" H"
    else
        if [ $patches -lt 10 ]; then
            cnt=" $patches"
        else
            cnt="$patches"
        fi
    fi
    /usr/bin/git show --abbrev-commit -s --pretty=format:"$cnt %h %<|(32)%an %s %n" $i
    patches=$(echo $patches - 1 | bc)
done
#git log --reverse --abbrev-commit --pretty=format:"%h %<|(32)%an %s" $tracking..$branch
#git log --reverse --abbrev-commit --pretty=format:"%h %<|(32)%an %s" ^origin/master ^linux-gfs2/for-next $branch
```

#### 2、gitlog.id

```
#!/bin/bash
branch=$1

if test "x$branch" = x; then
    branch=`git branch -a | grep "*" | cut -d ' ' -f2`
fi

tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`

/usr/bin/echo "-----------------------[" $branch "]-----------------------"
git log --reverse --abbrev-commit --pretty=oneline $tracking..$branch | cut -d ' ' -f1 |paste -s -d ' '
```

#### 3、gitlog.id2

```
#!/bin/bash
branch=$1

if test "x$branch" = x; then
    branch=`git branch -a | grep "*" | cut -d ' ' -f2`
fi

tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`
git log --reverse --abbrev-commit --pretty=oneline $tracking..$branch | cut -d ' ' -f1 |paste -s -d ' '
```

#### 4、gitlog.grep

```
#!/bin/bash
param1=$1
param2=$2

if test "x$param2" = x; then
    branch=`git branch -a | grep "*" | cut -d ' ' -f2`
    string=$param1
else
    branch=$param1
    string=$param2
fi

patches=0
tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`

LIST=`git log --reverse --abbrev-commit --pretty=oneline $tracking..$branch | cut -d ' ' -f1 |paste -s -d ' '`
for i in $LIST; do patches=$(echo $patches + 1 | bc);done
/usr/bin/echo "-----------------------[" $branch "-" $patches "patches ]-----------------------"
patches=$(echo $patches - 1 | bc);
for i in $LIST; do
    if [ $patches -eq 1 ]; then
        cnt=" ^"
    elif [ $patches -eq 0 ]; then
        cnt=" H"
    else
        if [ $patches -lt 10 ]; then
            cnt=" $patches"
        else
            cnt="$patches"
        fi
    fi
    /usr/bin/git show --abbrev-commit -s --pretty=format:"$cnt %h %<|(32)%an %s" $i
    /usr/bin/git show --pretty=email --patch-with-stat $i | grep -n "$string"
    patches=$(echo $patches - 1 | bc)
done
```

#### 5、gitbranchcmp3

```
#!/bin/bash
#
# gitbranchcmp3 <old branch> [<new_branch>]
#
oldbranch=$1
newbranch=$2
script=/tmp/compare_mismatches.sh

/usr/bin/rm -f $script
echo "#!/bin/bash" > $script
/usr/bin/chmod 755 $script
echo "# Generated by gitbranchcmp3.sh" >> $script
echo "# Run this script to compare the mismatched patches" >> $script
echo " " >> $script
echo "function compare_them()" >> $script
echo "{"  >> $script
echo "    git show --pretty=email --patch-with-stat \$1 > /tmp/gronk1" >> $script
echo "    git show --pretty=email --patch-with-stat \$2 > /tmp/gronk2" >> $script
echo "    kompare /tmp/gronk1 /tmp/gronk2" >> $script
echo "}" >> $script
echo " " >> $script

if test "x$newbranch" = x; then
    newbranch=`git branch -a | grep "*" | cut -d ' ' -f2`
fi

tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`

declare -a oldsha1s=(`git log --reverse --abbrev-commit --pretty=oneline $tracking..$oldbranch | cut -d ' ' -f1 |paste -s -d ' '`)
declare -a newsha1s=(`git log --reverse --abbrev-commit --pretty=oneline $tracking..$newbranch | cut -d ' ' -f1 |paste -s -d ' '`)

#echo "old: " $oldsha1s
oldcount=${#oldsha1s[@]}
echo "Branch $oldbranch has $oldcount patches"
oldcount=$(echo $oldcount - 1 | bc)
#for o in `seq 0 ${#oldsha1s[@]}`; do
#    echo -n ${oldsha1s[$o]} " "
#    desc=`git show $i | head -5 | tail -1|cut -b5-`
#done

#echo "new: " $newsha1s
newcount=${#newsha1s[@]}
echo "Branch $newbranch has $newcount patches"
newcount=$(echo $newcount - 1 | bc)
#for o in `seq 0 ${#newsha1s[@]}`; do
#    echo -n ${newsha1s[$o]} " "
#    desc=`git show $i | head -5 | tail -1|cut -b5-`
#done
echo

for new in `seq 0 $newcount`; do
    newsha=${newsha1s[$new]}
    newdesc=`git show $newsha | head -5 | tail -1|cut -b5-`
    oldsha="            "
    same="[    ]"
    for old in `seq 0 $oldcount`; do
        if test "${oldsha1s[$old]}" = "match"; then
            continue;
        fi
        olddesc=`git show ${oldsha1s[$old]} | head -5 | tail -1|cut -b5-`
        if test "$olddesc" = "$newdesc" ; then
            oldsha=${oldsha1s[$old]}
            #echo $oldsha
            git show $oldsha |tail -n +2 |grep -v "index.*\.\." |grep -v "@@" > /tmp/gronk1
            git show $newsha |tail -n +2 |grep -v "index.*\.\." |grep -v "@@"  > /tmp/gronk2
            diff /tmp/gronk1 /tmp/gronk2 &> /dev/null
            if [ $? -eq 0 ] ;then
# No differences
                same="[SAME]"
                oldsha1s[$old]="match"
                break
            fi
            git show $oldsha |sed -n '/diff/,$p' |grep -v "index.*\.\." |grep -v "@@" > /tmp/gronk1
            git show $newsha |sed -n '/diff/,$p' |grep -v "index.*\.\." |grep -v "@@" > /tmp/gronk2
            diff /tmp/gronk1 /tmp/gronk2 &> /dev/null
            if [ $? -eq 0 ] ;then
# Differences in comments only
                same="[same]"
                oldsha1s[$old]="match"
                break
            fi
            oldsha1s[$old]="match"
            echo "compare_them $oldsha $newsha" >> $script
        fi
    done
    echo "$new $oldsha $newsha $same $newdesc"
done

echo
echo "Missing from $newbranch:"
the_missing=""
# Now run through the olds we haven't matched up
for old in `seq 0 $oldcount`; do
    if test ${oldsha1s[$old]} != "match"; then
        olddesc=`git show ${oldsha1s[$old]} | head -5 | tail -1|cut -b5-`
        echo "${oldsha1s[$old]} $olddesc"
        the_missing=`echo "$the_missing ${oldsha1s[$old]}"`
    fi
done

echo "The missing: " $the_missing
echo "Compare script generated at: $script"
#git log --reverse --abbrev-commit --pretty=oneline $tracking..$branch | cut -d ' ' -f1 |paste -s -d ' '
```

#### 6、gitlog.find

```
#!/bin/bash
#
# Find the upstream equivalent patch
#
# gitlog.find
#
cwd=$PWD
param1=$1
ubranch=$2
patches=0
script=/tmp/compare_upstream.sh
echo "#!/bin/bash" > $script
/usr/bin/chmod 755 $script
echo "# Generated by gitbranchcmp3.sh" >> $script
echo "# Run this script to compare the mismatched patches" >> $script
echo " " >> $script
echo "function compare_them()" >> $script
echo "{"  >> $script
echo "    cwd=$PWD" >> $script
echo "    git show --pretty=email --patch-with-stat \$2 > /tmp/gronk2" >> $script
echo "    cd ~/linux.git/fs/gfs2" >> $script
echo "    git show --pretty=email --patch-with-stat \$1 > /tmp/gronk1" >> $script
echo "    cd $cwd" >> $script
echo "    kompare /tmp/gronk1 /tmp/gronk2" >> $script
echo "}" >> $script
echo " " >> $script

#echo "Gathering upstream patch info. Please wait."
branch=`git branch -a | grep "*" | cut -d ' ' -f2`
tracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`

cd ~/linux.git
if test "X${ubranch}" = "X"; then
    ubranch=`git branch -a | grep "*" | cut -d ' ' -f2`
fi
utracking=`git rev-parse --abbrev-ref --symbolic-full-name @{u}`
#
# gather a list of gfs2 patches from master just in case we can't find it
#
#git log --abbrev-commit --pretty=format:"   %h %<|(32)%an %s" master |grep -i -e "gfs2" -e "dlm" > /tmp/gronk
git log --reverse --abbrev-commit --pretty=format:"ms %h %<|(32)%an %s" master fs/gfs2/ > /tmp/gronk.gfs2
# ms = in Linus's master
git log --reverse --abbrev-commit --pretty=format:"ms %h %<|(32)%an %s" master fs/dlm/ > /tmp/gronk.dlm

cd $cwd
LIST=`git log --reverse --abbrev-commit --pretty=oneline $tracking..$branch | cut -d ' ' -f1 |paste -s -d ' '`
for i in $LIST; do patches=$(echo $patches + 1 | bc);done
/usr/bin/echo "-----------------------[" $branch "-" $patches "patches ]-----------------------"
patches=$(echo $patches - 1 | bc);
for i in $LIST; do
    if [ $patches -eq 1 ]; then
        cnt=" ^"
    elif [ $patches -eq 0 ]; then
        cnt=" H"
    else
        if [ $patches -lt 10 ]; then
            cnt=" $patches"
        else
            cnt="$patches"
        fi
    fi
    /usr/bin/git show --abbrev-commit -s --pretty=format:"$cnt %h %<|(32)%an %s" $i
    desc=`/usr/bin/git show --abbrev-commit -s --pretty=format:"%s" $i`
    cd ~/linux.git
    cmp=1
    up_eq=`git log --reverse --abbrev-commit --pretty=format:"lo %h %<|(32)%an %s" $utracking..$ubranch | grep "$desc"`
# lo = in local for-next
    if test "X$up_eq" = "X"; then
        up_eq=`git log --reverse --abbrev-commit --pretty=format:"fn %h %<|(32)%an %s" master..$utracking | grep "$desc"`
# fn = in for-next for next merge window
        if test "X$up_eq" = "X"; then
            up_eq=`grep "$desc" /tmp/gronk.gfs2`
            if test "X$up_eq" = "X"; then
                up_eq=`grep "$desc" /tmp/gronk.dlm`
                if test "X$up_eq" = "X"; then
                    up_eq="   Not found upstream"
                    cmp=0
                fi
            fi
        fi
    fi
    echo "$up_eq"
    if [ $cmp -eq 1 ] ; then
        UP_SHA1=`echo $up_eq|cut -d' ' -f2`
        echo "compare_them $UP_SHA1 $i" >> $script
    fi
    cd $cwd
    patches=$(echo $patches - 1 | bc)
done
echo "Compare script generated: $script"
```

---

via: <https://opensource.com/article/20/1/bash-scripts-git>

作者：[Bob Peterson](https://opensource.com/users/bobpeterson) 选题：[lujun9972](https://github.com/lujun9972) 译者：[wxy](https://github.com/wxy) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


在 GIMP 中如何在文本周围添加边框

Summary: 这个简单的教程介绍了在 GIMP 中显示文本的轮廓的步骤。文本轮廓可以帮助你在其它颜色下高亮显示该文本。

![](/data/attachment/album/202001/19/230506fzkyktqglfcyzkuh.jpg)

这个简单的教程介绍了在 [GIMP](https://www.gimp.org/) 中显示文本的轮廓的步骤。文本轮廓可以帮助你在其它颜色下高亮显示该文本。

![Outlined Text created in GIMP](/data/attachment/album/202001/19/230013erdd1bmr7c1ft1bs.png)

让我们看看如何在你的文本周围添加一个边框。

### 在 GIMP 中添加文本轮廓

整个过程可以用这些简单的步骤描述：

* 创建文本，并复制它的轮廓路径
* 添加一层新的透明层，并添加轮廓路径到透明层中
* 更改轮廓的大小，给它添加一种不同的颜色

这就是全部的东西。不用担心，我将使用适当地截图详细的展示每个步骤。按照这个教程，你应该能够为文本添加轮廓，即使你在此之前从未使用过 GIMP 。

仅需要确保你已经 [在 Linux 上安装 GIMP](https://itsfoss.com/gimp-2-10-release/)，或者也可以使用的其它任何操作系统。

这篇教程在 GIMP 2.10 版本下演示。

#### 步骤 1: 创建你的主要文本，并复制它的轮廓

打开 GIMP ，并通过转到 “菜单 -> 文件 -> 新建” 来创建一个新的文件。你应该可以使用 `Ctrl+N` 键盘快捷键。

![Create New File](/data/attachment/album/202001/19/230016sr0m18p2xv55xp5v.jpg)

你可以在这里选择画布的大小。你也可以选择要白色背景或一种透明背景。它在 “高级选项 -> 颜色” 配置文件下。

我选择默认的白色背景。它在以后能够更改。

现在从左边栏的工具箱中选择文本工具。

![Adding text in GIMP](/data/attachment/album/202001/19/230019r6dg2lz2ukgg2u22.jpg)

写你想要的文本。你可以根据你的选择以更改文本的字体、大小和对齐方式。我保持这篇文章的文本的默认左对齐。

我故意为文本选择一种浅色，以便难于阅读。在这篇教程中我将添加一个深色轮廓到这个浅色的文本。

![Text added in GIMP](/data/attachment/album/202001/19/230020n92jvyu2tytt8ana.jpg)

当你写完文本后，右键文本框并选择 “文本的路径” 。

![Right click on the text box and select ‘Path from Text’](/data/attachment/album/202001/19/230026xmgodo2u82jmbc29.jpg)

#### 步骤 2: 添加一个带有文本轮廓的透明层

现在，转到顶部菜单，转到“层”，并添加一个新层。

![Use Shift+Ctrl+N to add a new layer](/data/attachment/album/202001/19/230027ld5g2jgpggl5558l.jpg)

确保添加新层为透明的。你可以给它一个合适的名称，像“文本大纲”。单击确定来添加这个透明层。

![Add a transparent layer](/data/attachment/album/202001/19/230031kawgaaaarercsitt.jpg)

再次转到菜单，这次转到 “选择” ，并单击 “来自路径” 。你将看到你的文本应该被高亮显示。

![Go to Select and choose From Path](/data/attachment/album/202001/19/230032yxetlf9qrbruer6u.jpg)

总的来说，你只创建了一个透明层，它有像你的原文一样相同的文本（但是透明）。现在你需要做的是在这个层上增加文本的大小。

#### 步骤 3: 通过增加它的大小和更改它的颜色来添加文本轮廓

为此，再次在菜单中转到 “选择” ，这次选择 “增加”。这将允许增大透明层上的文本的大小。

![Grow the selection on the additional layer](/data/attachment/album/202001/19/230033mgvfd9qqzdm00qfh.jpg)

以 5 或 10 像素增加，或者你喜欢的任意像素。

![Grow it by 5 or 10 pixel](/data/attachment/album/202001/19/230034dl9717clelz7lrj7.jpg)

你选择需要做是使用一种你选择的颜色来填充这个扩大的选择区。因为我的原文是浅色，在这里我将为轮廓使用背景色。

如果尚未选择的话，先选择你的主图像层。这些层在右侧栏中可视。然后转到工具箱并选择油漆桶工具。为你的轮廓选择想要的颜色。

选择使用该工具来填充黑色到你的选择区。记住。你填充文本外部的轮廓，而不是文本本身。

![Fill the outline of the text with a different color](/data/attachment/album/202001/19/230035hughofowg88z7l0w.jpg)

在这里你完成了很多。使用 `Ctrl+Shift+A` 来取消你当前的选择区。

![Outline added to the text](/data/attachment/album/202001/19/230041m19wrrpviuy6v1tc.jpg)

如此，你现在已经在 GIMP 中成功地添加轮廓到你的文本。它是在白色背景中，如果你想要一个透明背景，只需要在右侧栏的图层菜单中删除背景层。

![Remove the white background layer if you want a transparent background](/data/attachment/album/202001/19/230043xw934zbf30zrqoaf.jpg)

如果你对结果感到满意，保存文件未 PNG 文件(来保留透明背景)，或你喜欢的任何文件格式。

### 你使它工作了吗？

就这样。这就是你在 GIMP 中为添加一个文本轮廓而需要做的全部工作。

我希望你发现这个 GIMP 教程有帮助。你可能想查看另一个 [关于在 GIMP 中添加一个水印的简单教程](https://itsfoss.com/add-watermark-gimp-linux/)。

如果你有问题或建议，请在下面自由留言。

---

via: <https://itsfoss.com/gimp-text-outline/>

作者：[Abhishek Prakash](https://itsfoss.com/author/abhishek/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[robsean](https://github.com/robsean) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


setV：一个管理 Python 虚拟环境的 Bash 函数

>
> 了解一下 setV，它是一个轻量级的 Python 虚拟环境管理器，是 virtualenvwrapper 的替代产品。
>
>
>

![](/data/attachment/album/202001/19/234306tvvg5ffwakrzr5vv.jpg)

这一年多来，我的 [bash\_scripts](https://github.com/psachin/bash_scripts) 项目中悄悄隐藏这 [setV](https://gitlab.com/psachin/setV)，但现在是时候该公开了。setV 是一个 Bash 函数，我可以用它代替 [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/)。它提供了使你能够执行以下操作的基本功能：

* 默认使用 Python 3
* 创建一个新的虚拟环境
* 使用带有 `-p`（或 `--python`）的自定义 Python 路径来创建新的虚拟环境
* 删除现有的虚拟环境
* 列出所有现有的虚拟环境
* 使用制表符补全（以防你忘记虚拟环境名称）

### 安装

要安装 setV，请下载该脚本：

```
curl https://gitlab.com/psachin/setV/raw/master/install.sh
```

审核一下脚本，然后运行它：

```
sh ./install.sh
```

当安装 setV 时，安装脚本会要求你引入（`source`）一下 `~/.bashrc` 或 `~/.bash_profile` 的配置，根据你的喜好选择一个。

### 用法

基本的命令格式是 `setv`。

#### 创建虚拟环境

```
setv --new rango  # setv -n rango

# 或使用定制的 Python 路径
setv --new --python /opt/python/python3 rango  # setv -n -p /opt/python/python3 rango
```

#### 激活已有的虚拟环境

```
setv VIRTUAL_ENVIRONMENT_NAME
```

```
# 示例
setv rango
```

#### 列出所有的虚拟环境

```
setv --list
# 或
setv [TAB] [TAB]
```

#### 删除虚拟环境

```
setv --delete rango
```

#### 切换到另外一个虚拟环境

```
# 假设你现在在 'rango'，切换到 'tango'
setv tango
```

#### 制表符补完

如果你不完全记得虚拟环境的名称，则 Bash 式的制表符补全也可以适用于虚拟环境名称。

### 参与其中

setV 在 GNU [GPLv3](https://gitlab.com/psachin/setV/blob/master/LICENSE)下开源，欢迎贡献。要了解更多信息，请访问它的 GitLab 存储库中的 setV 的 [README](https://gitlab.com/psachin/setV/blob/master/ReadMe.org) 的贡献部分。

### setV 脚本

```
#!/usr/bin/env bash
# setV - A Lightweight Python virtual environment manager.
# Author: Sachin (psachin) <iclcoolster@gmail.com>
# Author's URL: https://psachin.gitlab.io/about
#
# License: GNU GPL v3, See LICENSE file
#
# Configure(Optional):
# Set `SETV_VIRTUAL_DIR_PATH` value to your virtual environments
# directory-path. By default it is set to '~/virtualenvs/'
#
# Usage:
# Manual install: Added below line to your .bashrc or any local rc script():
# ---
# source /path/to/virtual.sh
# ---
#
# Now you can 'activate' the virtual environment by typing
# $ setv <YOUR VIRTUAL ENVIRONMENT NAME>
#
# For example:
# $ setv rango
#
# or type:
# setv [TAB] [TAB]  (to list all virtual envs)
#
# To list all your virtual environments:
# $ setv --list
#
# To create new virtual environment:
# $ setv --new new_virtualenv_name
#
# To delete existing virtual environment:
# $ setv --delete existing_virtualenv_name
#
# To deactivate, type:
# $ deactivate

# Path to virtual environment directory
SETV_VIRTUAL_DIR_PATH="$HOME/virtualenvs/"
# Default python version to use. This decides whether to use `virtualenv` or `python3 -m venv`
SETV_PYTHON_VERSION=3  # Defaults to Python3
SETV_PY_PATH=$(which python${SETV_PYTHON_VERSION})

function _setvcomplete_()
{
    # Bash-autocompletion.
    # This ensures Tab-auto-completions work for virtual environment names.
    local cmd="${1##*/}" # to handle command(s).
                         # Not necessary as such. 'setv' is the only command

    local word=${COMP_WORDS[COMP_CWORD]} # Words thats being completed
    local xpat='${word}'                 # Filter pattern. Include
                                         # only words in variable '$names'
    local names=$(ls -l "${SETV_VIRTUAL_DIR_PATH}" | egrep '^d' | awk -F " " '{print $NF}') # Virtual environment names

    COMPREPLY=($(compgen -W "$names" -X "$xpat" -- "$word")) # compgen generates the results
}

function _setv_help_() {
    # Echo help/usage message
    echo "Usage: setv [OPTIONS] [NAME]"
    echo Positional argument:
    echo -e "NAME                       Activate virtual env."
    echo Optional arguments:
    echo -e "-l, --list                 List all Virtual Envs."
    echo -e "-n, --new NAME             Create a new Python Virtual Env."
    echo -e "-d, --delete NAME          Delete existing Python Virtual Env."
    echo -e "-p, --python PATH          Python binary path."
}

function _setv_custom_python_path()
{
    if [ -f "${1}" ];
    then
        if [ "`expr $1 : '.*python\([2,3]\)'`" = "3" ];
        then
            SETV_PYTHON_VERSION=3
        else
            SETV_PYTHON_VERSION=2
        fi
        SETV_PY_PATH=${1}
        _setv_create $2
    else
        echo "Error: Path ${1} does not exist!"
    fi
}

function _setv_create()
{
    # Creates new virtual environment if ran with -n|--new flag
    if [ -z ${1} ];
    then
        echo "You need to pass virtual environment name"
        _setv_help_
    else
        echo "Creating new virtual environment with the name: $1"

        if [ ${SETV_PYTHON_VERSION} -eq 3 ];
        then
            ${SETV_PY_PATH} -m venv ${SETV_VIRTUAL_DIR_PATH}${1}
        else
            virtualenv -p ${SETV_PY_PATH} ${SETV_VIRTUAL_DIR_PATH}${1}
        fi

        echo "You can now activate the Python virtual environment by typing: setv ${1}"
    fi
}

function _setv_delete()
{
    # Deletes virtual environment if ran with -d|--delete flag
    # TODO: Refactor
    if [ -z ${1} ];
    then
        echo "You need to pass virtual environment name"
        _setv_help_
    else
        if [ -d ${SETV_VIRTUAL_DIR_PATH}${1} ];
        then
            read -p "Really delete this virtual environment(Y/N)? " yes_no
            case $yes_no in
                Y|y) rm -rvf ${SETV_VIRTUAL_DIR_PATH}${1};;
                N|n) echo "Leaving the virtual environment as it is.";;
                *) echo "You need to enter either Y/y or N/n"
            esac
        else
            echo "Error: No virtual environment found by the name: ${1}"
        fi
    fi
}

function _setv_list()
{
    # Lists all virtual environments if ran with -l|--list flag
    echo -e "List of virtual environments you have under ${SETV_VIRTUAL_DIR_PATH}:\n"
    for virt in $(ls -l "${SETV_VIRTUAL_DIR_PATH}" | egrep '^d' | awk -F " " '{print $NF}')
    do
        echo ${virt}
    done
}

function setv() {
    # Main function
    if [ $# -eq 0 ];
    then
        _setv_help_
    elif [ $# -le 3 ];
    then
        case "${1}" in
            -n|--new) _setv_create ${2};;
            -d|--delete) _setv_delete ${2};;
            -l|--list) _setv_list;;
            *) if [ -d ${SETV_VIRTUAL_DIR_PATH}${1} ];
               then
                   # Activate the virtual environment
                   source ${SETV_VIRTUAL_DIR_PATH}${1}/bin/activate
               else
                   # Else throw an error message
                   echo "Sorry, you don't have any virtual environment with the name: ${1}"
                   _setv_help_
               fi
               ;;
        esac
    elif [ $# -le 5 ];
    then
        case "${2}" in
            -p|--python) _setv_custom_python_path ${3} ${4};;
            *) _setv_help_;;
        esac
    fi
}

# Calls bash-complete. The compgen command accepts most of the same
# options that complete does but it generates results rather than just
# storing the rules for future use.
complete  -F _setvcomplete_ setv
```

---

via: <https://opensource.com/article/20/1/setv-bash-function>

作者：[Sachin Patil](https://opensource.com/users/psachin) 选题：[lujun9972](https://github.com/lujun9972) 译者：[wxy](https://github.com/wxy) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出