分类 技术 下的文章

这是我建立一个简单的 Facebook Messenger 机器人的记录。功能很简单,它是一个回显机器人,只是打印回用户写了什么。

回显服务器类似于服务器的“Hello World”例子。

这个项目的目的不是建立最好的 Messenger 机器人,而是让你了解如何建立一个小型机器人和每个事物是如何整合起来的。

技术栈

使用到的技术栈:

  • Heroku 做后端主机。免费级足够这个等级的教程。回显机器人不需要任何种类的数据持久,所以不需要数据库。
  • Python 是我们选择的语言。版本选择 2.7,虽然它移植到 Pyhton 3 很容易,只需要很少的改动。
  • Flask 作为网站开发框架。它是非常轻量的框架,用在小型工程或微服务是非常完美的。
  • 最后 Git 版本控制系统用来维护代码和部署到 Heroku。
  • 值得一提:Virtualenv。这个 python 工具是用来创建清洁的 python 库“环境”的,这样你可以只安装必要的需求和最小化应用的大小。

机器人架构

Messenger 机器人是由一个响应两种请求的服务器组成的:

  • GET 请求被用来认证。他们与你注册的 FaceBook 认证码一同被 Messenger 发出。
  • POST 请求被用来实际的通信。典型的工作流是,机器人将通过用户发送带有消息数据的 POST 请求而建立通信,然后我们将处理这些数据,并发回我们的 POST 请求。如果这个请求完全成功(返回一个 200 OK 状态码),我们也将响应一个 200 OK 状态码给初始的 Messenger请求。

这个教程应用将托管到 Heroku,它提供了一个优雅而简单的部署应用的接口。如前所述,免费级可以满足这个教程。

在应用已经部署并且运行后,我们将创建一个 Facebook 应用然后连接它到我们的应用,以便 Messenger 知道发送请求到哪,这就是我们的机器人。

机器人服务器

基本的服务器代码可以在 Github 用户 hult(Magnus Hult)Chatbot 项目上获取,做了一些只回显消息的代码修改和修正了一些我遇到的错误。最终版本的服务器代码如下:

from flask import Flask, request
import json
import requests

app = Flask(__name__)

### 这需要填写被授予的页面通行令牌(PAT)
### 它由将要创建的 Facebook 应用提供。
PAT = ''

@app.route('/', methods=['GET'])
def handle_verification():
  print "Handling Verification."
  if request.args.get('hub.verify_token', '') == 'my_voice_is_my_password_verify_me':
    print "Verification successful!"
    return request.args.get('hub.challenge', '')
  else:
    print "Verification failed!"
    return 'Error, wrong validation token'

@app.route('/', methods=['POST'])
def handle_messages():
  print "Handling Messages"
  payload = request.get_data()
  print payload
  for sender, message in messaging_events(payload):
    print "Incoming from %s: %s" % (sender, message)
    send_message(PAT, sender, message)
  return "ok"

def messaging_events(payload):
  """Generate tuples of (sender_id, message_text) from the
  provided payload.
  """
  data = json.loads(payload)
  messaging_events = data["entry"][0]["messaging"]
  for event in messaging_events:
    if "message" in event and "text" in event["message"]:
      yield event["sender"]["id"], event["message"]["text"].encode('unicode_escape')
    else:
      yield event["sender"]["id"], "I can't echo this"


def send_message(token, recipient, text):
  """Send the message text to recipient with id recipient.
  """

  r = requests.post("https://graph.facebook.com/v2.6/me/messages",
    params={"access_token": token},
    data=json.dumps({
      "recipient": {"id": recipient},
      "message": {"text": text.decode('unicode_escape')}
    }),
    headers={'Content-type': 'application/json'})
  if r.status_code != requests.codes.ok:
    print r.text

if __name__ == '__main__':
  app.run()

让我们分解代码。第一部分是引入所需的依赖:

from flask import Flask, request
import json
import requests

接下来我们定义两个函数(使用 Flask 特定的 app.route 装饰器),用来处理到我们的机器人的 GET 和 POST 请求。

@app.route('/', methods=['GET'])
def handle_verification():
  print "Handling Verification."
  if request.args.get('hub.verify_token', '') == 'my_voice_is_my_password_verify_me':
    print "Verification successful!"
    return request.args.get('hub.challenge', '')
  else:
    print "Verification failed!"
    return 'Error, wrong validation token'

当我们创建 Facebook 应用时,verify\_token 对象将由我们声明的 Messenger 发送。我们必须自己来校验它。最后我们返回“hub.challenge”给 Messenger。

处理 POST 请求的函数更有意思一些:

@app.route('/', methods=['POST'])
def handle_messages():
  print "Handling Messages"
  payload = request.get_data()
  print payload
  for sender, message in messaging_events(payload):
    print "Incoming from %s: %s" % (sender, message)
    send_message(PAT, sender, message)
  return "ok"

当被调用时,我们抓取消息载荷,使用函数 messaging\_events 来拆解它,并且提取发件人身份和实际发送的消息,生成一个可以循环处理的 python 迭代器。请注意 Messenger 发送的每个请求有可能多于一个消息。

def messaging_events(payload):
  """Generate tuples of (sender_id, message_text) from the
  provided payload.
  """
  data = json.loads(payload)
  messaging_events = data["entry"][0]["messaging"]
  for event in messaging_events:
    if "message" in event and "text" in event["message"]:
      yield event["sender"]["id"], event["message"]["text"].encode('unicode_escape')
    else:
      yield event["sender"]["id"], "I can't echo this"

对每个消息迭代时,我们会调用 send\_message 函数,然后我们使用 Facebook Graph messages API 对 Messenger 发回 POST 请求。在这期间我们一直没有回应我们阻塞的原始 Messenger请求。这会导致超时和 5XX 错误。

上述情况是我在解决遇到错误时发现的,当用户发送表情时实际上是发送的 unicode 标识符,但是被 Python 错误的编码了,最终我们发回了一些乱码。

这个发回 Messenger 的 POST 请求将永远不会完成,这会导致给初始请求返回 5xx 状态码,显示服务不可用。

通过使用 encode('unicode_escape') 封装消息,然后在我们发送回消息前用 decode('unicode_escape') 解码消息就可以解决。

def send_message(token, recipient, text):
  """Send the message text to recipient with id recipient.
  """

  r = requests.post("https://graph.facebook.com/v2.6/me/messages",
    params={"access_token": token},
    data=json.dumps({
      "recipient": {"id": recipient},
      "message": {"text": text.decode('unicode_escape')}
    }),
    headers={'Content-type': 'application/json'})
  if r.status_code != requests.codes.ok:
    print r.text

部署到 Heroku

一旦代码已经建立成我想要的样子时就可以进行下一步。部署应用。

那么,该怎么做?

我之前在 Heroku 上部署过应用(主要是 Rails),然而我总是遵循某种教程做的,所用的配置是创建好了的。而在本文的情况下,我就需要从头开始。

幸运的是有官方 Heroku 文档来帮忙。这篇文档很好地说明了运行应用程序所需的最低限度。

长话短说,我们需要的除了我们的代码还有两个文件。第一个文件是“requirements.txt”,它列出了运行应用所依赖的库。

需要的第二个文件是“Procfile”。这个文件通知 Heroku 如何运行我们的服务。此外这个文件只需要一点点内容:

web: gunicorn echoserver:app 

Heroku 对它的解读是,我们的应用通过运行 echoserver.py 启动,并且应用将使用 gunicorn 作为 Web 服务器。我们使用一个额外的网站服务器是因为与性能相关,在上面的 Heroku 文档里对此解释了:

Web 应用程序并发处理传入的 HTTP 请求比一次只处理一个请求的 Web 应用程序会更有效利地用测试机的资源。由于这个原因,我们建议使用支持并发请求的 Web 服务器来部署和运行产品级服务。

Django 和 Flask web 框架提供了一个方便的内建 Web 服务器,但是这些阻塞式服务器一个时刻只能处理一个请求。如果你部署这种服务到 Heroku 上,你的测试机就会资源利用率低下,应用会感觉反应迟钝。

Gunicorn 是一个纯 Python 的 HTTP 服务器,用于 WSGI 应用。允许你在单独一个测试机内通过运行多 Python 进程的方式来并发的运行各种 Python 应用。它在性能、灵活性和配置简易性方面取得了完美的平衡。

回到我们之前提到过的“requirements.txt”文件,让我们看看它如何结合 Virtualenv 工具。

很多情况下,你的开发机器也许已经安装了很多 python 库。当部署应用时你不想全部加载那些库,但是辨认出你实际使用哪些库很困难。

Virtualenv 可以创建一个新的空白虚拟环境,以便你可以只安装你应用所需要的库。

你可以运行如下命令来检查当前安装了哪些库:

kostis@KostisMBP ~ $ pip freeze
cycler==0.10.0
Flask==0.10.1
gunicorn==19.6.0
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
matplotlib==1.5.1
numpy==1.10.4
pyparsing==2.1.0
python-dateutil==2.5.0
pytz==2015.7
requests==2.10.0
scipy==0.17.0
six==1.10.0
virtualenv==15.0.1
Werkzeug==0.11.10

注意:pip 工具应该已经与 Python 一起安装在你的机器上。如果没有,查看官方网站如何安装它。

现在让我们使用 Virtualenv 来创建一个新的空白环境。首先我们给我们的项目创建一个新文件夹,然后进到目录下:

kostis@KostisMBP projects $ mkdir echoserver
kostis@KostisMBP projects $ cd echoserver/
kostis@KostisMBP echoserver $

现在来创建一个叫做 echobot 的新环境。运行下面的 source 命令激活它,然后使用 pip freeze 检查,我们能看到现在是空的。

kostis@KostisMBP echoserver $ virtualenv echobot
kostis@KostisMBP echoserver $ source echobot/bin/activate
(echobot) kostis@KostisMBP echoserver $ pip freeze
(echobot) kostis@KostisMBP echoserver $

我们可以安装需要的库。我们需要是 flask、gunicorn 和 requests,它们被安装后我们就创建 requirements.txt 文件:

(echobot) kostis@KostisMBP echoserver $ pip install flask
(echobot) kostis@KostisMBP echoserver $ pip install gunicorn
(echobot) kostis@KostisMBP echoserver $ pip install requests
(echobot) kostis@KostisMBP echoserver $ pip freeze
click==6.6
Flask==0.11
gunicorn==19.6.0
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
requests==2.10.0
Werkzeug==0.11.10
(echobot) kostis@KostisMBP echoserver $ pip freeze > requirements.txt

上述完成之后,我们用 python 代码创建 echoserver.py 文件,然后用之前提到的命令创建 Procfile,我们最终的文件/文件夹如下:

(echobot) kostis@KostisMBP echoserver $ ls
Procfile     echobot     echoserver.py   requirements.txt

我们现在准备上传到 Heroku。我们需要做两件事。第一是如果还没有安装 Heroku toolbet,就安装它(详见 Heroku)。第二是通过 Heroku 网页界面创建一个新的 Heroku 应用。

点击右上的大加号然后选择“Create new app”。

为你的应用选择一个名字,然后点击“Create App”。

你将会重定向到你的应用的控制面板,在那里你可以找到如何部署你的应用到 Heroku 的细节说明。

(echobot) kostis@KostisMBP echoserver $ heroku login
(echobot) kostis@KostisMBP echoserver $ git init
(echobot) kostis@KostisMBP echoserver $ heroku git:remote -a <myappname>
(echobot) kostis@KostisMBP echoserver $ git add .
(echobot) kostis@KostisMBP echoserver $ git commit -m "Initial commit"
(echobot) kostis@KostisMBP echoserver (master) $ git push heroku master
...
remote:        https://<myappname>.herokuapp.com/ deployed to Heroku
...
(echobot) kostis@KostisMBP echoserver (master) $ heroku config:set WEB_CONCURRENCY=3

如上,当你推送你的修改到 Heroku 之后,你会得到一个用于公开访问你新创建的应用的 URL。保存该 URL,下一步需要它。

创建这个 Facebook 应用

让我们的机器人可以工作的最后一步是创建这个我们将连接到其上的 Facebook 应用。Facebook 通常要求每个应用都有一个相关页面,所以我们来创建一个

接下来我们去 Facebook 开发者专页,点击右上角的“My Apps”按钮并选择“Add a New App”。不要选择建议的那个,而是点击“basic setup”。填入需要的信息并点击“Create App Id”,然后你会重定向到新的应用页面。

在 “Products” 菜单之下,点击“+ Add Product” ,然后在“Messenger”下点击“Get Started”。跟随这些步骤设置 Messenger,当完成后你就可以设置你的 webhooks 了。Webhooks 简单的来说是你的服务所用的 URL 的名称。点击 “Setup Webhooks” 按钮,并添加该 Heroku 应用的 URL (你之前保存的那个)。在校验元组中写入 ‘myvoiceismypasswordverifyme’。你可以写入任何你要的内容,但是不管你在这里写入的是什么内容,要确保同时修改代码中 handle\_verification 函数。然后勾选 “messages” 选项。

点击“Verify and Save” 就完成了。Facebook 将访问该 Heroku 应用并校验它。如果不工作,可以试试运行:

(echobot) kostis@KostisMBP heroku logs -t

然后看看日志中是否有错误。如果发现错误, Google 搜索一下可能是最快的解决方法。

最后一步是取得页面访问元组(PAT),它可以将该 Facebook 应用于你创建好的页面连接起来。

从下拉列表中选择你创建好的页面。这会在“Page Access Token”(PAT)下面生成一个字符串。点击复制它,然后编辑 echoserver.py 文件,将其贴入 PAT 变量中。然后在 Git 中添加、提交并推送该修改。

(echobot) kostis@KostisMBP echoserver (master) $ git add .
(echobot) kostis@KostisMBP echoserver (master) $ git commit -m "Initial commit"
(echobot) kostis@KostisMBP echoserver (master) $ git push heroku master

最后,在 Webhooks 菜单下再次选择你的页面并点击“Subscribe”。

现在去访问你的页面并建立会话:

成功了,机器人回显了!

注意:除非你要将这个机器人用在 Messenger 上测试,否则你就是机器人唯一响应的那个人。如果你想让其他人也试试它,到 Facebook 开发者专页中,选择你的应用、角色,然后添加你要添加的测试者。

总结

这对于我来说是一个非常有用的项目,希望它可以指引你找到开始的正确方向。官方的 Facebook 指南有更多的资料可以帮你学到更多。

你可以在 Github 上找到该项目的代码。

如果你有任何评论、勘误和建议,请随时联系我。


via: http://tsaprailis.com/2016/06/02/How-to-build-and-deploy-a-Facebook-Messenger-bot-with-Python-and-Flask-a-tutorial/

作者:Konstantinos Tsaprailis 译者:wyangsun 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出

在这个系列的介绍篇中,我们学习到了谁应该使用 Git,以及 Git 是用来做什么的。今天,我们将学习如何克隆公共 Git 仓库,以及如何提取出独立的文件而不用克隆整个仓库。

由于 Git 如此流行,因而如果你能够至少熟悉一些基础的 Git 知识也能为你的生活带来很多便捷。如果你可以掌握 Git 基础(你可以的,我发誓!),那么你将能够下载任何你需要的东西,甚至还可能做一些贡献作为回馈。毕竟,那就是开源的精髓所在:你拥有获取你使用的软件代码的权利,拥有和他人分享的自由,以及只要你愿意就可以修改它的权利。只要你熟悉了 Git,它就可以让这一切都变得很容易。

那么,让我们一起来熟悉 Git 吧。

读和写

一般来说,有两种方法可以和 Git 仓库交互:你可以从仓库中读取,或者你也能够向仓库中写入。它就像一个文件:有时候你打开一个文档只是为了阅读它,而其它时候你打开文档是因为你需要做些改动。

本文仅讲解如何从 Git 仓库读取。我们将会在后面的一篇文章中讲解如何向 Git 仓库写回的主题。

Git 还是 GitHub?

一句话澄清:Git 不同于 GitHub(或 GitLab,或 Bitbucket)。Git 是一个命令行程序,所以它就像下面这样:

$ git
usage: Git [--version] [--help] [-C <path>] 
  [-p | --paginate | --no-pager] [--bare]
  [--Git-dir=<path>] <command> [<args>]

由于 Git 是开源的,所以就有许多聪明人围绕它构建了基础软件;这些基础软件,包括在他们自己身边,都已经变得非常流行了。

我的文章系列将首先教你纯粹的 Git 知识,因为一旦你理解了 Git 在做什么,那么你就无需关心正在使用的前端工具是什么了。然而,我的文章系列也将涵盖通过流行的 Git 服务完成每项任务的常用方法,因为那些将可能是你首先会遇到的。

安装 Git

在 Linux 系统上,你可以从所使用的发行版软件仓库中获取并安装 Git。BSD 用户应当在 Ports 树的 devel 部分查找 Git。

对于闭源的操作系统,请前往其项目官网,并根据说明安装。一旦安装后,在 Linux、BSD 和 Mac OS X 上的命令应当没有任何差别。Windows 用户需要调整 Git 命令,从而和 Windows 文件系统相匹配,或者安装 Cygwin 以原生的方式运行 Git,而不受 Windows 文件系统转换问题的羁绊。

Git 下午茶

并非每个人都需要立刻将 Git 加入到我们的日常生活中。有些时候,你和 Git 最多的交互就是访问一个代码库,下载一两个文件,然后就不用它了。以这样的方式看待 Git,它更像是下午茶而非一次正式的宴会。你进行一些礼节性的交谈,获得了需要的信息,然后你就会离开,至少接下来的三个月你不再想这样说话。

当然,那是可以的。

一般来说,有两种方法访问 Git:使用命令行,或者使用一种神奇的因特网技术通过 web 浏览器快速轻松地访问。

假设你想要给终端安装一个回收站,因为你已经被 rm 命令毁掉太多次了。你可能听说过 Trashy ,它称自己为「理智的 rm 命令中间人」,也许你想在安装它之前阅读它的文档。幸运的是,Trashy 公开地托管在 GitLab.com

Landgrab

我们工作的第一步是对这个 Git 仓库使用 landgrab 排序方法:我们会克隆这个完整的仓库,然后会根据内容排序。由于该仓库是托管在公共的 Git 服务平台上,所以有两种方式来完成工作:使用命令行,或者使用 web 界面。

要想使用 Git 获取整个仓库,就要使用 git clone 命令和 Git 仓库的 URL 作为参数。如果你不清楚正确的 URL 是什么,仓库应该会告诉你的。GitLab 为你提供了 Trashy 仓库的用于拷贝粘贴的 URL。

你也许注意到了,在某些服务平台上,会同时提供 SSH 和 HTTPS 链接。只有当你拥有仓库的写权限时,你才可以使用 SSH。否则的话,你必须使用 HTTPS URL。

一旦你获得了正确的 URL,克隆仓库是非常容易的。就是 git clone 该 URL 即可,以及一个可选的指定要克隆到的目录。默认情况下会将 git 目录克隆到你当前所在的目录;例如,'trashy.git' 将会克隆到你当前位置的 'trashy' 目录。我使用 .clone 扩展名标记那些只读的仓库,而使用 .git 扩展名标记那些我可以读写的仓库,不过这并不是官方要求的。

$ git clone https://gitlab.com/trashy/trashy.git trashy.clone
Cloning into 'trashy.clone'...
remote: Counting objects: 142, done.
remote: Compressing objects: 100% (91/91), done.
remote: Total 142 (delta 70), reused 103 (delta 47)
Receiving objects: 100% (142/142), 25.99 KiB | 0 bytes/s, done.
Resolving deltas: 100% (70/70), done.
Checking connectivity... done.

一旦成功地克隆了仓库,你就可以像对待你电脑上任何其它目录那样浏览仓库中的文件。

另外一种获得仓库拷贝的方式是使用 web 界面。GitLab 和 GitHub 都会提供一个 .zip 格式的仓库快照文件。GitHub 有一个大大的绿色下载按钮,但是在 GitLab 中,可以在浏览器的右侧找到并不显眼的下载按钮。

仔细挑选

另外一种从 Git 仓库中获取文件的方法是找到你想要的文件,然后把它从仓库中拽出来。只有 web 界面才提供这种方法,本质上来说,你看到的是别人的仓库克隆;你可以把它想象成一个 HTTP 共享目录。

使用这种方法的问题是,你也许会发现某些文件并不存在于原始仓库中,因为完整形式的文件可能只有在执行 make 命令后才能构建,那只有你下载了完整的仓库,阅读了 README 或者 INSTALL 文件,然后运行相关命令之后才会产生。不过,假如你确信文件存在,而你只想进入仓库,获取那个文件,然后离开的话,你就可以那样做。

在 GitLab 和 GitHub 中,单击文件链接,并在 Raw 模式下查看,然后使用你的 web 浏览器的保存功能,例如:在 Firefox 中,“文件” > “保存页面为”。在一个 GitWeb 仓库中(这是一个某些更喜欢自己托管 git 的人使用的私有 git 仓库 web 查看器),Raw 查看链接在文件列表视图中。

最佳实践

通常认为,和 Git 交互的正确方式是克隆完整的 Git 仓库。这样认为是有几个原因的。首先,可以使用 git pull 命令轻松地使克隆仓库保持更新,这样你就不必在每次文件改变时就重回 web 站点获得一份全新的拷贝。第二,你碰巧需要做些改进,只要保持仓库整洁,那么你可以非常轻松地向原来的作者提交所做的变更。

现在,可能是时候练习查找感兴趣的 Git 仓库,然后将它们克隆到你的硬盘中了。只要你了解使用终端的基础知识,那就不会太难做到。还不知道基本的终端使用方式吗?那再给多我 5 分钟时间吧。

终端使用基础

首先要知道的是,所有的文件都有一个路径。这是有道理的;如果我让你在常规的非终端环境下为我打开一个文件,你就要导航到文件在你硬盘的位置,并且直到你找到那个文件,你要浏览一大堆窗口。例如,你也许要点击你的家目录 > 图片 > InktoberSketches > monkey.kra。

在那样的场景下,文件 monkeysketch.kra 的路径是:$HOME/图片/InktoberSketches/monkey.kra。

在终端中,除非你正在处理一些特殊的系统管理员任务,你的文件路径通常是以 $HOME 开头的(或者,如果你很懒,就使用 ~ 字符),后面紧跟着一些列的文件夹直到文件名自身。

这就和你在 GUI 中点击各种图标直到找到相关的文件或文件夹类似。

如果你想把 Git 仓库克隆到你的文档目录,那么你可以打开一个终端然后运行下面的命令:

$ git clone https://gitlab.com/foo/bar.git 
$HOME/文档/bar.clone

一旦克隆完成,你可以打开一个文件管理器窗口,导航到你的文档文件夹,然后你就会发现 bar.clone 目录正在等待着你访问。

如果你想要更高级点,你或许会在以后再次访问那个仓库,可以尝试使用 git pull 命令来查看项目有没有更新:

$ cd $HOME/文档/bar.clone
$ pwd
bar.clone
$ git pull

到目前为止,你需要初步了解的所有终端命令就是那些了,那就去探索吧。你实践得越多,Git 掌握得就越好(熟能生巧),这是重点,也是事情的本质。


via: https://opensource.com/life/16/7/stumbling-git

作者:Seth Kenlon 译者:ChrisLeeGit 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出

欢迎阅读本系列关于如何使用 Git 版本控制系统的教程!通过本文的介绍,你将会了解到 Git 的用途及谁该使用 Git。

如果你刚步入开源的世界,你很有可能会遇到一些在 Git 上托管代码或者发布使用版本的开源软件。事实上,不管你知道与否,你都在使用基于 Git 进行版本管理的软件:Linux 内核(就算你没有在手机或者电脑上使用 Linux,你正在访问的网站也是运行在 Linux 系统上的),Firefox、Chrome 等其他很多项目都通过 Git 代码库和世界各地开发者共享他们的代码。

换个角度来说,你是否仅仅通过 Git 就可以和其他人共享你的代码?你是否可以在家里或者企业里私有化的使用 Git?你必须要通过一个 GitHub 账号来使用 Git 吗?为什么要使用 Git 呢?Git 的优势又是什么?Git 是我唯一的选择吗?这对 Git 所有的疑问都会把我们搞的一脑浆糊。

因此,忘记你以前所知的 Git,让我们重新走进 Git 世界的大门。

什么是版本控制系统?

Git 首先是一个版本控制系统。现在市面上有很多不同的版本控制系统:CVS、SVN、Mercurial、Fossil 当然还有 Git。

很多像 GitHub 和 GitLab 这样的服务是以 Git 为基础的,但是你也可以只使用 Git 而无需使用其他额外的服务。这意味着你可以以私有或者公有的方式来使用 Git。

如果你曾经和其他人有过任何电子文件方面的合作,你就会知道传统版本管理的工作流程。开始是很简单的:你有一个原始的版本,你把这个版本发送给你的同事,他们在接收到的版本上做了些修改,现在你们有两个版本了,然后他们把他们手上修改过的版本发回来给你。你把他们的修改合并到你手上的版本中,现在两个版本又合并成一个最新的版本了。

然后,你修改了你手上最新的版本,同时,你的同事也修改了他们手上合并前的版本。现在你们有 3 个不同的版本了,分别是合并后最新的版本,你修改后的版本,你同事手上继续修改过的版本。至此,你们的版本管理工作开始变得越来越混乱了。

正如 Jason van Gumster 在他的文章中指出 即使是艺术家也需要版本控制,而且已经在个别人那里发现了这种趋势变化。无论是艺术家还是科学家,开发一个某种实验版本是并不鲜见的;在你的项目中,可能有某个版本大获成功,把项目推向一个新的高度,也可能有某个版本惨遭失败。因此,最终你不可避免的会创建出一堆名为project\_justTesting.kdenlive、project\_betterVersion.kdenlive、project\_best\_FINAL.kdenlive、project\_FINAL-alternateVersion.kdenlive 等类似名称的文件。

不管你是修改一个 for 循环,还是一些简单的文本编辑,一个好的版本控制系统都会让我们的生活更加的轻松。

Git 快照

Git 可以为项目创建快照,并且存储这些快照为唯一的版本。

如果你将项目带领到了一个错误的方向上,你可以回退到上一个正确的版本,并且开始尝试另一个可行的方向。

如果你是和别人合作开发,当有人向你发送他们的修改时,你可以将这些修改合并到你的工作分支中,然后你的同事就可以获取到合并后的最新版本,并在此基础上继续工作。

Git 并不是魔法,因此冲突还是会发生的(“你修改了某文件的最后一行,但是我把这行整行都删除了;我们怎样处理这些冲突呢?”),但是总体而言,Git 会为你保留了所有更改的历史版本,甚至允许并行版本。这为你保留了以任何方式处理冲突的能力。

分布式 Git

在不同的机器上为同一个项目工作是一件复杂的事情。因为在你开始工作时,你想要获得项目的最新版本,然后此基础上进行修改,最后向你的同事共享这些改动。传统的方法是通过笨重的在线文件共享服务或者老旧的电邮附件,但是这两种方式都是效率低下且容易出错。

Git 天生是为分布式工作设计的。如果你要参与到某个项目中,你可以 克隆 clone 该项目的 Git 仓库,然后就像这个项目只有你本地一个版本一样对项目进行修改。最后使用一些简单的命令你就可以 拉取 pull 其他开发者的修改,或者你可以把你的修改 推送 push 给别人。现在不用担心谁手上的是最新的版本,或者谁的版本又存放在哪里等这些问题了。全部人都是在本地进行开发,然后向共同的目标推送或者拉取更新。(或者不是共同的目标,这取决于项目的开发方式)。

Git 界面

最原始的 Git 是运行在 Linux 终端上的应用软件。然而,得益于 Git 是开源的,并且拥有良好的设计,世界各地的开发者都可以为 Git 设计不同的访问界面。

Git 完全是免费的,并且已经打包在 Linux,BSD,Illumos 和其他类 Unix 系统中,Git 命令看起来像这样:

$ git --version
git version 2.5.3

可能最著名的 Git 访问界面是基于网页的,像 GitHub、开源的 GitLab、Savannah、BitBucket 和 SourceForge 这些网站都是基于网页端的 Git 界面。这些站点为面向公众和面向社会的开源软件提供了最大限度的代码托管服务。在一定程度上,基于浏览器的图形界面(GUI)可以尽量的减缓 Git 的学习曲线。下面的 GitLab 界面的截图:

再者,第三方 Git 服务提供商或者独立开发者甚至可以在 Git 的基础上开发出不是基于 HTML 的定制化前端界面。此类界面让你可以不用打开浏览器就可以方便的使用 Git 进行版本管理。其中对用户最透明的方式是直接集成到文件管理器中。KDE 文件管理器 Dolphin 可以直接在目录中显示 Git 状态,甚至支持提交,推送和拉取更新操作。

Sparkleshare 使用 Git 作为其 Dropbox 式的文件共享界面的基础。

想了解更多的内容,可以查看 Git wiki,这个(长长的)页面中展示了很多 Git 的图形界面项目。

谁应该使用 Git?

就是你!我们更应该关心的问题是什么时候使用 Git?和用 Git 来干嘛?

我应该在什么时候使用 Git 呢?我要用 Git 来干嘛呢?

想更深入的学习 Git,我们必须比平常考虑更多关于文件格式的问题。

Git 是为了管理源代码而设计的,在大多数编程语言中,源代码就意味者一行行的文本。当然,Git 并不知道你把这些文本当成是源代码还是下一部伟大的美式小说。因此,只要文件内容是以文本构成的,使用 Git 来跟踪和管理其版本就是一个很好的选择了。

但是什么是文本呢?如果你在像 Libre Office 这类办公软件中编辑一些内容,通常并不会产生纯文本内容。因为通常复杂的应用软件都会对原始的文本内容进行一层封装,就如把原始文本内容用 XML 标记语言包装起来,然后封装在 Zip 包中。这种对原始文本内容进行一层封装的做法可以保证当你把文件发送给其他人时,他们可以看到你在办公软件中编辑的内容及特定的文本效果。奇怪的是,虽然,通常你的需求可能会很复杂,就像保存 Kdenlive 项目文件,或者保存从 Inkscape 导出的SVG文件,但是,事实上使用 Git 管理像 XML 文本这样的纯文本类容是最简单的。

如果你在使用 Unix 系统,你可以使用 file 命令来查看文件内容构成:

$ file ~/path/to/my-file.blah
my-file.blah: ASCII text
$ file ~/path/to/different-file.kra: Zip data (MIME type "application/x-krita")

如果还是不确定,你可以使用 head 命令来查看文件内容:

$ head ~/path/to/my-file.blah

如果输出的文本你基本能看懂,这个文件就很有可能是文本文件。如果你仅仅在一堆乱码中偶尔看到几个熟悉的字符,那么这个文件就可能不是文本文件了。

准确的说:Git 可以管理其他格式的文件,但是它会把这些文件当成二进制大对象(blob)。两者的区别是,在文本文件中,Git 可以明确的告诉你在这两个快照(或者说提交)间有 3 行是修改过的。但是如果你在两个 提交 commit 之间对一张图片进行的编辑操作,Git 会怎么指出这种修改呢?实际上,因为图片并不是以某种可以增加或删除的有意义的文本构成,因此 Git 并不能明确的描述这种变化。当然我个人是非常希望图片的编辑可以像把文本“丑陋的蓝绿色”修改成“漂浮着蓬松白云的天蓝色”一样的简单,但是事实上图片的编辑并没有这么简单。

经常有人在 Git 上放入 png 图标、电子表格或者流程图这类二进制大型对象(blob)。尽管,我们知道在 Git 上管理此类大型文件并不直观,但是,如果你需要使用 Git 来管理此类文件,你也并不需要过多的担心。如果你参与的项目同时生成文本文件和二进制大文件对象(如视频游戏中常见的场景,这些和源代码同样重要的图像和音频材料),那么你有两条路可以走:要么开发出你自己的解决方案,就如使用指向共享网络驱动器的引用;要么使用 Git 插件,如 Joey Hess 开发的 git annex,以及 Git-Media 项目。

你看,Git 真的是一个任何人都可以使用的工具。它是你进行文件版本管理的一个强大而且好用工具,同时它并没有你开始认为的那么可怕。


via: https://opensource.com/resources/what-is-git

作者:Seth Kenlon 译者:cvsher 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

上篇中,我们已经创建了一个 shell 主循环、切分了命令输入,以及通过 forkexec 执行命令。在这部分,我们将会解决剩下的问题。首先,cd test_dir2 命令无法修改我们的当前目录。其次,我们仍无法优雅地从 shell 中退出。

步骤 4:内置命令

cd test_dir2 无法修改我们的当前目录” 这句话是对的,但在某种意义上也是错的。在执行完该命令之后,我们仍然处在同一目录,从这个意义上讲,它是对的。然而,目录实际上已经被修改,只不过它是在子进程中被修改。

还记得我们分叉(fork)了一个子进程,然后执行命令,执行命令的过程没有发生在父进程上。结果是我们只是改变了子进程的当前目录,而不是父进程的目录。

然后子进程退出,而父进程在原封不动的目录下继续运行。

因此,这类与 shell 自己相关的命令必须是内置命令。它必须在 shell 进程中执行而不是在分叉中(forking)。

cd

让我们从 cd 命令开始。

我们首先创建一个 builtins 目录。每一个内置命令都会被放进这个目录中。

yosh_project
|-- yosh
   |-- builtins
   |   |-- __init__.py
   |   |-- cd.py
   |-- __init__.py
   |-- shell.py

cd.py 中,我们通过使用系统调用 os.chdir 实现自己的 cd 命令。

import os
from yosh.constants import *

def cd(args):
    os.chdir(args[0])

    return SHELL_STATUS_RUN

注意,我们会从内置函数返回 shell 的运行状态。所以,为了能够在项目中继续使用常量,我们将它们移至 yosh/constants.py

yosh_project
|-- yosh
   |-- builtins
   |   |-- __init__.py
   |   |-- cd.py
   |-- __init__.py
   |-- constants.py
   |-- shell.py

constants.py 中,我们将状态常量都放在这里。

SHELL_STATUS_STOP = 0
SHELL_STATUS_RUN = 1

现在,我们的内置 cd 已经准备好了。让我们修改 shell.py 来处理这些内置函数。

...
### 导入常量
from yosh.constants import *

### 使用哈希映射来存储内建的函数名及其引用
built_in_cmds = {}

def tokenize(string):
    return shlex.split(string)

def execute(cmd_tokens):
    ### 从元组中分拆命令名称与参数
    cmd_name = cmd_tokens[0]
    cmd_args = cmd_tokens[1:]

    ### 如果该命令是一个内建命令,使用参数调用该函数
    if cmd_name in built_in_cmds:
        return built_in_cmds[cmd_name](cmd_args)

    ...

我们使用一个 python 字典变量 built_in_cmds 作为 哈希映射 hash map ,以存储我们的内置函数。我们在 execute 函数中提取命令的名字和参数。如果该命令在我们的哈希映射中,则调用对应的内置函数。

(提示:built_in_cmds[cmd_name] 返回能直接使用参数调用的函数引用。)

我们差不多准备好使用内置的 cd 函数了。最后一步是将 cd 函数添加到 built_in_cmds 映射中。

...
### 导入所有内建函数引用
from yosh.builtins import *

...

### 注册内建函数到内建命令的哈希映射中
def register_command(name, func):
    built_in_cmds[name] = func


### 在此注册所有的内建命令
def init():
    register_command("cd", cd)


def main():
    ###在开始主循环之前初始化 shell
    init()
    shell_loop()

我们定义了 register_command 函数,以添加一个内置函数到我们内置的命令哈希映射。接着,我们定义 init 函数并且在这里注册内置的 cd 函数。

注意这行 register_command("cd", cd) 。第一个参数为命令的名字。第二个参数为一个函数引用。为了能够让第二个参数 cd 引用到 yosh/builtins/cd.py 中的 cd 函数引用,我们必须将以下这行代码放在 yosh/builtins/__init__.py 文件中。

from yosh.builtins.cd import *

因此,在 yosh/shell.py 中,当我们从 yosh.builtins 导入 * 时,我们可以得到已经通过 yosh.builtins 导入的 cd 函数引用。

我们已经准备好了代码。让我们尝试在 yosh 同级目录下以模块形式运行我们的 shell,python -m yosh.shell

现在,cd 命令可以正确修改我们的 shell 目录了,同时非内置命令仍然可以工作。非常好!

exit

最后一块终于来了:优雅地退出。

我们需要一个可以修改 shell 状态为 SHELL_STATUS_STOP 的函数。这样,shell 循环可以自然地结束,shell 将到达终点而退出。

cd 一样,如果我们在子进程中分叉并执行 exit 函数,其对父进程是不起作用的。因此,exit 函数需要成为一个 shell 内置函数。

让我们从这开始:在 builtins 目录下创建一个名为 exit.py 的新文件。

yosh_project
|-- yosh
   |-- builtins
   |   |-- __init__.py
   |   |-- cd.py
   |   |-- exit.py
   |-- __init__.py
   |-- constants.py
   |-- shell.py

exit.py 定义了一个 exit 函数,该函数仅仅返回一个可以退出主循环的状态。

from yosh.constants import *

def exit(args):
    return SHELL_STATUS_STOP

然后,我们导入位于 yosh/builtins/__init__.py 文件的 exit 函数引用。

from yosh.builtins.cd import *
from yosh.builtins.exit import *

最后,我们在 shell.py 中的 init() 函数注册 exit 命令。

...

### 在此注册所有的内建命令
def init():
    register_command("cd", cd)
    register_command("exit", exit)

...

到此为止!

尝试执行 python -m yosh.shell。现在你可以输入 exit 优雅地退出程序了。

最后的想法

我希望你能像我一样享受创建 yoshyour own shell)的过程。但我的 yosh 版本仍处于早期阶段。我没有处理一些会使 shell 崩溃的极端状况。还有很多我没有覆盖的内置命令。为了提高性能,一些非内置命令也可以实现为内置命令(避免新进程创建时间)。同时,大量的功能还没有实现(请看 公共特性不同特性)。

我已经在 https://github.com/supasate/yosh 中提供了源代码。请随意 fork 和尝试。

现在该是创建你真正自己拥有的 Shell 的时候了。

Happy Coding!


via: https://hackercollider.com/articles/2016/07/06/create-your-own-shell-in-python-part-2/

作者:Supasate Choochaisri 译者:cposture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

欢迎来到“Fedora 中的容器技术”系列!本文是该系列文章中的第一篇,它将说明你可以怎样使用 Fedora 中各种可用的容器技术。本文将学习 systemd-nspawn 的相关知识。

容器是什么?

一个容器就是一个用户空间实例,它能够在与托管容器的系统(叫做宿主系统)相隔离的环境中运行一个程序或者一个操作系统。这和 chroot虚拟机 的思想非常类似。运行在容器中的进程是由与宿主操作系统相同的内核来管理的,但它们是与宿主文件系统以及其它进程隔离开的。

什么是 systemd-nspawn?

systemd 项目认为应当将容器技术变成桌面的基础部分,并且应当和用户的其余系统集成在一起。为此,systemd 提供了 systemd-nspawn,这款工具能够使用多种 Linux 技术创建容器。它也提供了一些容器管理工具。

systemd-nspawnchroot 在许多方面都是类似的,但是前者更加强大。它虚拟化了文件系统、进程树以及客户系统中的进程间通信。它的吸引力在于它提供了很多用于管理容器的工具,例如用来管理容器的 machinectl。由 systemd-nspawn 运行的容器将会与 systemd 组件一同运行在宿主系统上。举例来说,一个容器的日志可以输出到宿主系统的日志中。

在 Fedora 24 上,systemd-nspawn 已经从 systemd 软件包分离出来了,所以你需要安装 systemd-container 软件包。一如往常,你可以使用 dnf install systemd-container 进行安装。

创建容器

使用 systemd-nspawn 创建一个容器是很容易的。假设你有一个专门为 Debian 创造的应用,并且无法在其它发行版中正常运行。那并不是一个问题,我们可以创造一个容器!为了设置容器使用最新版本的 Debian(现在是 Jessie),你需要挑选一个目录来放置你的系统。我暂时将使用目录 ~/DebianJessie

一旦你创建完目录,你需要运行 debootstrap,你可以从 Fedora 仓库中安装它。对于 Debian Jessie,你运行下面的命令来初始化一个 Debian 文件系统。

$ debootstrap --arch=amd64 stable ~/DebianJessie

以上默认你的架构是 x86\_64。如果不是的话,你必须将架构的名称改为 amd64。你可以使用 uname -m 得知你的机器架构。

一旦设置好你的根目录,你就可以使用下面的命令来启动你的容器。

$ systemd-nspawn -bD ~/DebianJessie

容器将会在数秒后准备好并运行,当你试图登录时就会注意到:你无法使用你的系统上任何账户。这是因为 systemd-nspawn 虚拟化了用户。修复的方法很简单:将之前的命令中的 -b 移除即可。你将直接进入容器的 root 用户的 shell。此时,你只能使用 passwd 命令为 root 设置密码,或者使用 adduser 命令添加一个新用户。一旦设置好密码或添加好用户,你就可以把 -b 标志添加回去然后继续了。你会进入到熟悉的登录控制台,然后你使用设置好的认证信息登录进去。

以上对于任意你想在容器中运行的发行版都适用,但前提是你需要使用正确的包管理器创建系统。对于 Fedora,你应使用 DNF 而非 debootstrap。想要设置一个最小化的 Fedora 系统,你可以运行下面的命令,要将“/absolute/path/”替换成任何你希望容器存放的位置。

$ sudo dnf --releasever=24 --installroot=/absolute/path/ install systemd passwd dnf fedora-release

设置网络

如果你尝试启动一个服务,但它绑定了你宿主机正在使用的端口,你将会注意到这个问题:你的容器正在使用和宿主机相同的网络接口。幸运的是,systemd-nspawn 提供了几种可以将网络从宿主机分开的方法。

本地网络

第一种方法是使用 --private-network 标志,它默认仅创建一个回环设备。这对于你不需要使用网络的环境是非常理想的,例如构建系统和其它持续集成系统。

多个网络接口

如果你有多个网络接口设备,你可以使用 --network-interface 标志给容器分配一个接口。想要给我的容器分配 eno1,我会添加选项 --network-interface=eno1。当某个接口分配给一个容器后,宿主机就不能同时使用那个接口了。只有当容器彻底关闭后,宿主机才可以使用那个接口。

共享网络接口

对于我们中那些并没有额外的网络设备的人来说,还有其它方法可以访问容器。一种就是使用 --port 选项。这会将容器中的一个端口定向到宿主机。使用格式是 协议:宿主机端口:容器端口,这里的协议可以是 tcp 或者 udp宿主机端口 是宿主机的一个合法端口,容器端口 则是容器中的一个合法端口。你可以省略协议,只指定 宿主机端口:容器端口。我通常的用法类似 --port=2222:22

你可以使用 --network-veth 启用完全的、仅宿主机模式的网络,这会在宿主机和容器之间创建一个虚拟的网络接口。你也可以使用 --network-bridge 桥接二者的连接。

使用 systemd 组件

如果你容器中的系统含有 D-Bus,你可以使用 systemd 提供的实用工具来控制并监视你的容器。基础安装的 Debian 并不包含 dbus。如果你想在 Debian Jessie 中使用 dbus,你需要运行命令 apt install dbus

machinectl

为了能够轻松地管理容器,systemd 提供了 machinectl 实用工具。使用 machinectl,你可以使用 machinectl login name 登录到一个容器中、使用 machinectl status name检查状态、使用 machinectl reboot name 启动容器或者使用 machinectl poweroff name 关闭容器。

其它 systemd 命令

多数 systemd 命令,例如 journalctl, systemd-analyzesystemctl,都支持使用 --machine 选项来指定容器。例如,如果你想查看一个名为 “foobar” 的容器的日志,你可以使用 journalctl --machine=foobar。你也可以使用 systemctl --machine=foobar status service 来查看运行在这个容器中的服务状态。

和 SELinux 一起工作

如果你要使用 SELinux 强制模式(Fedora 默认模式),你需要为你的容器设置 SELinux 环境。想要那样的话,你需要在宿主系统上运行下面两行命令。

$ semanage fcontext -a -t svirt_sandbox_file_t "/path/to/container(/.*)?"
$ restorecon -R /path/to/container/

确保使用你的容器路径替换 “/path/to/container”。对于我的容器 "DebianJessie",我会运行下面的命令:

$ semanage fcontext -a -t svirt_sandbox_file_t "/home/johnmh/DebianJessie(/.*)?"
$ restorecon -R /home/johnmh/DebianJessie/

via: https://fedoramagazine.org/container-technologies-fedora-systemd-nspawn/

作者:John M. Harris, Jr. 译者:ChrisLeeGit 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我很想知道一个 shell (像 bash,csh 等)内部是如何工作的。于是为了满足自己的好奇心,我使用 Python 实现了一个名为 yosh (Your Own Shell)的 Shell。本文章所介绍的概念也可以应用于其他编程语言。

(提示:你可以在这里查找本博文使用的源代码,代码以 MIT 许可证发布。在 Mac OS X 10.11.5 上,我使用 Python 2.7.10 和 3.4.3 进行了测试。它应该可以运行在其他类 Unix 环境,比如 Linux 和 Windows 上的 Cygwin。)

让我们开始吧。

步骤 0:项目结构

对于此项目,我使用了以下的项目结构。

yosh_project
|-- yosh
   |-- __init__.py
   |-- shell.py

yosh_project 为项目根目录(你也可以把它简单命名为 yosh)。

yosh 为包目录,且 __init__.py 可以使它成为与包的目录名字相同的包(如果你不用 Python 编写的话,可以忽略它。)

shell.py 是我们主要的脚本文件。

步骤 1:Shell 循环

当启动一个 shell,它会显示一个命令提示符并等待你的命令输入。在接收了输入的命令并执行它之后(稍后文章会进行详细解释),你的 shell 会重新回到这里,并循环等待下一条指令。

shell.py 中,我们会以一个简单的 main 函数开始,该函数调用了 shell\_loop() 函数,如下:

def shell_loop():
    # Start the loop here

def main():
    shell_loop()

if __name__ == "__main__":
    main()

接着,在 shell_loop() 中,为了指示循环是否继续或停止,我们使用了一个状态标志。在循环的开始,我们的 shell 将显示一个命令提示符,并等待读取命令输入。

import sys

SHELL_STATUS_RUN = 1
SHELL_STATUS_STOP = 0

def shell_loop():
    status = SHELL_STATUS_RUN

    while status == SHELL_STATUS_RUN:
        ### 显示命令提示符
        sys.stdout.write('> ')
        sys.stdout.flush()

        ### 读取命令输入
        cmd = sys.stdin.readline()

之后,我们 切分命令 tokenize 输入并进行 执行 execute (我们即将实现 tokenizeexecute 函数)。

因此,我们的 shell\_loop() 会是如下这样:

import sys

SHELL_STATUS_RUN = 1
SHELL_STATUS_STOP = 0

def shell_loop():
    status = SHELL_STATUS_RUN

    while status == SHELL_STATUS_RUN:
        ### 显示命令提示符
        sys.stdout.write('> ')
        sys.stdout.flush()

        ### 读取命令输入
        cmd = sys.stdin.readline()

        ### 切分命令输入
        cmd_tokens = tokenize(cmd)

        ### 执行该命令并获取新的状态
        status = execute(cmd_tokens)

这就是我们整个 shell 循环。如果我们使用 python shell.py 启动我们的 shell,它会显示命令提示符。然而如果我们输入命令并按回车,它会抛出错误,因为我们还没定义 tokenize 函数。

为了退出 shell,可以尝试输入 ctrl-c。稍后我将解释如何以优雅的形式退出 shell。

步骤 2: 命令切分 tokenize

当用户在我们的 shell 中输入命令并按下回车键,该命令将会是一个包含命令名称及其参数的长字符串。因此,我们必须切分该字符串(分割一个字符串为多个元组)。

咋一看似乎很简单。我们或许可以使用 cmd.split(),以空格分割输入。它对类似 ls -a my_folder 的命令起作用,因为它能够将命令分割为一个列表 ['ls', '-a', 'my_folder'],这样我们便能轻易处理它们了。

然而,也有一些类似 echo "Hello World"echo 'Hello World' 以单引号或双引号引用参数的情况。如果我们使用 cmd.spilt,我们将会得到一个存有 3 个标记的列表 ['echo', '"Hello', 'World"'] 而不是 2 个标记的列表 ['echo', 'Hello World']

幸运的是,Python 提供了一个名为 shlex 的库,它能够帮助我们如魔法般地分割命令。(提示:我们也可以使用正则表达式,但它不是本文的重点。)

import sys
import shlex

...

def tokenize(string):
    return shlex.split(string)

...

然后我们将这些元组发送到执行进程。

步骤 3:执行

这是 shell 中核心而有趣的一部分。当 shell 执行 mkdir test_dir 时,到底发生了什么?(提示: mkdir 是一个带有 test_dir 参数的执行程序,用于创建一个名为 test_dir 的目录。)

execvp 是这一步的首先需要的函数。在我们解释 execvp 所做的事之前,让我们看看它的实际效果。

import os
...

def execute(cmd_tokens):
    ### 执行命令
    os.execvp(cmd_tokens[0], cmd_tokens)

    ### 返回状态以告知在 shell_loop 中等待下一个命令
    return SHELL_STATUS_RUN

...

再次尝试运行我们的 shell,并输入 mkdir test_dir 命令,接着按下回车键。

在我们敲下回车键之后,问题是我们的 shell 会直接退出而不是等待下一个命令。然而,目录正确地创建了。

因此,execvp 实际上做了什么?

execvp 是系统调用 exec 的一个变体。第一个参数是程序名字。v 表示第二个参数是一个程序参数列表(参数数量可变)。p 表示将会使用环境变量 PATH 搜索给定的程序名字。在我们上一次的尝试中,它将会基于我们的 PATH 环境变量查找mkdir 程序。

(还有其他 exec 变体,比如 execv、execvpe、execl、execlp、execlpe;你可以 google 它们获取更多的信息。)

exec 会用即将运行的新进程替换调用进程的当前内存。在我们的例子中,我们的 shell 进程内存会被替换为 mkdir 程序。接着,mkdir 成为主进程并创建 test_dir 目录。最后该进程退出。

这里的重点在于我们的 shell 进程已经被 mkdir 进程所替换。这就是我们的 shell 消失且不会等待下一条命令的原因。

因此,我们需要其他的系统调用来解决问题:fork

fork 会分配新的内存并拷贝当前进程到一个新的进程。我们称这个新的进程为子进程,调用者进程为父进程。然后,子进程内存会被替换为被执行的程序。因此,我们的 shell,也就是父进程,可以免受内存替换的危险。

让我们看看修改的代码。

...

def execute(cmd_tokens):
    ### 分叉一个子 shell 进程
    ### 如果当前进程是子进程,其 `pid` 被设置为 `0`
    ### 否则当前进程是父进程的话,`pid` 的值
    ### 是其子进程的进程 ID。
    pid = os.fork()

    if pid == 0:
    ### 子进程
        ### 用被 exec 调用的程序替换该子进程
        os.execvp(cmd_tokens[0], cmd_tokens)
    elif pid > 0:
    ### 父进程
        while True:
            ### 等待其子进程的响应状态(以进程 ID 来查找)
            wpid, status = os.waitpid(pid, 0)

            ### 当其子进程正常退出时
            ### 或者其被信号中断时,结束等待状态
            if os.WIFEXITED(status) or os.WIFSIGNALED(status):
                break

    ### 返回状态以告知在 shell_loop 中等待下一个命令
    return SHELL_STATUS_RUN

...

当我们的父进程调用 os.fork() 时,你可以想象所有的源代码被拷贝到了新的子进程。此时此刻,父进程和子进程看到的是相同的代码,且并行运行着。

如果运行的代码属于子进程,pid 将为 0。否则,如果运行的代码属于父进程,pid 将会是子进程的进程 id。

os.execvp 在子进程中被调用时,你可以想象子进程的所有源代码被替换为正被调用程序的代码。然而父进程的代码不会被改变。

当父进程完成等待子进程退出或终止时,它会返回一个状态,指示继续 shell 循环。

运行

现在,你可以尝试运行我们的 shell 并输入 mkdir test_dir2。它应该可以正确执行。我们的主 shell 进程仍然存在并等待下一条命令。尝试执行 ls,你可以看到已创建的目录。

但是,这里仍有一些问题。

第一,尝试执行 cd test_dir2,接着执行 ls。它应该会进入到一个空的 test_dir2 目录。然而,你将会看到目录并没有变为 test_dir2

第二,我们仍然没有办法优雅地退出我们的 shell。

我们将会在下篇解决诸如此类的问题。


via: https://hackercollider.com/articles/2016/07/05/create-your-own-shell-in-python-part-1/

作者:Supasate Choochaisri 译者:cposture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出