2020年10月

有时我会觉得自己的计算机是一栋非常大的房子,我每天都会访问这栋房子,也对一楼的大部分房间都了如指掌,但仍然还是有我没有去过的卧室,有我没有打开过的衣柜,有我没有探索过的犄角旮旯。我感到有必要更多地了解我的计算机了,就像任何人都会觉得有必要看看自己家里从未去过的房间一样。

GNU Readline 是个不起眼的小软件库,我依赖了它多年却没有意识到它的存在,也许有成千上万的人每天都在不经意间使用它。如果你用 Bash shell 的话,每当你自动补全一个文件名,或者在输入的一行文本中移动光标,以及搜索之前命令的历史记录时,你都在使用 GNU Readline;当你在 Postgres(psql)或是 Ruby REPL(irb)的命令行界面中进行同样的操作时,你依然在使用 GNU Readline。很多软件都依赖 GNU Readline 库来实现用户所期望的功能,不过这些功能是如此的辅助与不显眼,以至于在我看来很少有人会停下来去想它是从哪里来的。

GNU Readline 最初是自由软件基金会在 20 世纪 80 年代创建的,如今作为每个人的基础计算设施的重要的、甚至看不见的组成部分的它,由一位志愿者维护。

充满特色

GNU Readline 库的存在,主要是为了增强各种命令行界面,它提供了一组通用的按键,使你可以在一个单行输入中移动和编辑。例如,在 Bash 提示符中按下 Ctrl-A,你的光标会跳到行首,而按下 Ctrl-E 则会跳到行末;另一个有用的命令是 Ctrl-U,它会删除该行中光标之前的所有内容。

有很长一段时间,我通过反复敲击方向键来在命令行上移动,如今看来这十分尴尬,也不知道为什么,当时的我从来没有想过可以有一种更快的方法。当然了,没有哪一个熟悉 Vim 或 Emacs 这种文本编辑器的程序员愿意长时间地击打方向键,所以像 Readline 这样的东西必然会被创造出来。在 Readline 上可以做的绝非仅仅跳来跳去,你可以像使用文本编辑器那样编辑单行文本——这里有删除单词、单词换位、大写单词、复制和粘贴字符等命令。Readline 的大部分按键/快捷键都是基于 Emacs 的,它基本上就是一个单行文本版的 Emacs 了,甚至还有录制和重放宏的功能。

我从来没有用过 Emacs,所以很难记住所有不同的 Readline 命令。不过 Readline 有着很巧妙的一点,那就是能够切换到基于 Vim 的模式,在 Bash 中可以使用内置的 set 命令来这样做。下面会让 Readline 在当前的 shell 中使用 Vim 风格的命令:

$ set -o vi

该选项启用后,就可以使用 dw 等命令来删除单词了,此时相当于 Emacs 模式下的 Ctrl-U 的命令是 d0

我第一次知道有这个功能的时候很兴奋地想尝试一下,但它对我来说并不是那么好用。我很高兴知道有这种对 Vim 用户的让步,在使用这个功能上你可能会比我更幸运,尤其是你还没有使用 Readline 的默认按键的话;我的问题在于,我听说有基于 Vim 的界面时已经学会了几种默认按键,因此即使启用了 Vim 的选项,也一直在错误地用着默认的按键;另外因为没有某种指示器,所以 Vim 的模态设计在这里会很尴尬——你很容易就忘记了自己处于哪个模式,就因为这样,我卡在了一种虽然使用 Vim 作为文本编辑器,但却在 Readline 上用着 Emacs 风格的命令的情况里,我猜其他很多人也是这样的。

如果你觉得 Vim 和 Emacs 的键盘命令系统诡异而神秘(这并不是没有道理的),你可以按照喜欢的方式自定义 Readline 的键绑定。Readline 在启动时会读取文件 ~/.inputrc,它可以用来配置各种选项与键绑定,我做的一件事是重新配置了 Ctrl-K:通常情况下该命令会从光标处删除到行末,但我很少这样做,所以我在 ~/.inputrc 中添加了以下内容,把它绑定为直接删除整行:

Control-k: kill-whole-line

每个 Readline 命令(文档中称它们为 “函数” )都有一个名称,你可以用这种方式将其与一个键序列联系起来。如果你在 Vim 中编辑 ~/.inputrc,就会发现 Vim 知道这种文件类型,还会帮你高亮显示有效的函数名,而不高亮无效的函数名。

~/.inputrc 可以做的另一件事是通过将键序列映射到输入字符串上来创建预制宏。Readline 手册给出了一个我认为特别有用的例子:我经常想把一个程序的输出保存到文件中,这意味着我得经常在 Bash 命令中追加类似 > output.txt 这样的东西,为了节省时间,可以把它做成一个 Readline 宏:

Control-o: "> output.txt"

这样每当你按下 Ctrl-O 时,你都会看到 > output.txt 被添加到了命令行光标的后面,这样很不错!

不过你可以用宏做的可不仅仅是为文本串创建快捷方式;在 ~/.inputrc 中使用以下条目意味着每次按下 Ctrl-J 时,行内已有的文本都会被 $() 包裹住。该宏先用 Ctrl-A 移动到行首,添加 $( ,然后再用 Ctrl-E 移动到行尾,添加 )

Control-j: "\C-a$(\C-e)"

如果你经常需要像下面这样把一个命令的输出用于另一个命令的话,这个宏可能会对你有帮助:

$ cd $(brew --prefix)

~/.inputrc 文件也允许你为 Readline 手册中所谓的 “变量” 设置不同的值,这些变量会启用或禁用某些 Readline 行为,你也可以使用这些变量来改变 Readline 中像是自动补全或者历史搜索这些行为的工作方式。我建议开启的一个变量是 revert-all-at-newline,它是默认关闭的,当这个变量关闭时,如果你使用反向搜索功能从命令历史记录中提取一行并编辑,但随后又决定搜索另一行,那么你所做的编辑会被保存在历史记录中。我觉得这样会很混乱,因为这会导致你的 Bash 命令历史中出现从未运行过的行。所以在你的 ~/.inputrc 中加入这个:

set revert-all-at-newline on

在你用 ~/.inputrc 设置了选项或键绑定以后,它们会适用于任何使用 Readline 库的地方,显然 Bash 也包括在内,不过你也会在其它像是 irbpsql 这样的程序中受益。如果你经常使用关系型数据库的命令行界面,一个用于插入 SELECT * FROM 的 Readline 宏可能会很有用。

Chet Ramey

GNU Readline 如今由凯斯西储大学的高级技术架构师 Chet Ramey 维护,Ramey 同时还负责维护 Bash shell;这两个项目都是由一位名叫 Brian Fox 的自由软件基金会员工在 1988 年开始编写的,但从 1994 年左右开始,Ramey 一直是它们唯一的维护者。

Ramey 通过电子邮件告诉我,Readline 远非一个原创的想法,它是为了实现 POSIX 规范所规定的功能而被创建的,而 POSIX 规范又是在 20 世纪 80 年代末被制定的。许多早期的 shell,包括 Korn shell 和至少一个版本的 Unix System V shell,都包含行编辑功能。1988 年版的 Korn shell(ksh88)提供了 Emacs 风格和 Vi/Vim 风格的编辑模式。据我从手册页中得知,Korn shell 会通过查看 VISUALEDITOR 环境变量来决定你使用的模式,这一点非常巧妙。POSIX 中指定 shell 功能的部分近似于 ksh88 的实现,所以 GNU Bash 也要实现一个类似的灵活的行编辑系统来保持兼容,因此就有了 Readline。

Ramey 第一次参与 Bash 开发时,Readline 还是 Bash 项目目录下的一个单一的源文件,它其实只是 Bash 的一部分;随着时间的推移,Readline 文件慢慢地成为了独立的项目,不过直到 1994 年(Readline 2.0 版本发布),Readline 才完全成为了一个独立的库。

Readline 与 Bash 密切相关,Ramey 也通常把 Readline 与 Bash 的发布配对,但正如我上面提到的,Readline 是一个可以被任何有命令行界面的软件使用的库,而且它真的很容易使用。下面是一个例子,虽然简单,但这就是在 C 程序中使用 Readline 的方法。向 readline() 函数传递的字符串参数就是你希望 Readline 向用户显示的提示符:

#include <stdio.h>
#include <stdlib.h>
#include "readline/readline.h"

int main(int argc, char** argv)
{
    char* line = readline("my-rl-example> ");
    printf("You entered: \"%s\"\n", line);

    free(line);

    return 0;
}

你的程序会把控制权交给 Readline,它会负责从用户那里获得一行输入(以这样的方式让用户可以做所有花哨的行编辑工作),一旦用户真正提交了这一行,Readline 就会把它返回给你。在我的库搜索路径中有 Readline 库,所以我可以通过调用以下内容来链接 Readline 库,从而编译上面的内容:

$ gcc main.c -lreadline

当然,Readline 的 API 比起那个单一的函数要丰富得多,任何使用它的人都可以对库的行为进行各种调整,库的用户(开发者)甚至可以添加新的函数,来让最终用户可以通过 ~/.inputrc 来配置它们,这意味着 Readline 非常容易扩展。但是据我所知,即使是 Bash ,虽然事先有很多配置,最终也会像上面的例子一样调用简单的 readline() 函数来获取输入。(参见 GNU Bash 源代码中的这一行,Bash 似乎在这里将获取输入的责任交给了 Readline)。

Ramey 现在已经在 Bash 和 Readline 上工作了二十多年,但他的工作却从来没有得到过报酬 —— 他一直都是一名志愿者。Bash 和 Readline 仍然在积极开发中,尽管 Ramey 说 Readline 的变化比 Bash 慢得多。我问 Ramey 作为这么多人使用的软件唯一的维护者是什么感觉,他说可能有几百万人在不知不觉中使用 Bash(因为每个苹果设备都运行 Bash),这让他担心一个破坏性的变化会造成多大的混乱,不过他已经慢慢习惯了所有这些人的想法。他还说他会继续在 Bash 和 Readline 上工作,因为在这一点上他已经深深地投入了,而且他也只是单纯地喜欢把有用的软件提供给世界。

你可以在 Chet Ramey 的网站上找到更多关于他的信息。

喜欢这篇文章吗?我会每四周写出一篇像这样的文章。关注推特帐号 @TwoBitHistory 或者订阅 RSS 来获取更新吧!


via: https://twobithistory.org/2019/08/22/readline.html

作者:Two-Bit History 选题:lujun9972 译者:rakino 校对:wxy

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

本文是该系列的第七篇。

现在我们已经完成了后端,让我们转到前端。 我将采用单页应用程序方案。

首先,我们创建一个 static/index.html 文件,内容如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Messenger</title>
    <link rel="shortcut icon" href="data:,">
    <link rel="stylesheet" href="/styles.css">
    <script src="/main.js" type="module"></script>
</head>
<body></body>
</html>

这个 HTML 文件必须为每个 URL 提供服务,并且使用 JavaScript 负责呈现正确的页面。

因此,让我们将注意力转到 main.go 片刻,然后在 main() 函数中添加以下路由:

router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))

type SPAFileSystem struct {
    fs http.FileSystem
}

func (spa SPAFileSystem) Open(name string) (http.File, error) {
    f, err := spa.fs.Open(name)
    if err != nil {
        return spa.fs.Open("index.html")
    }
    return f, nil
}

我们使用一个自定义的文件系统,因此它不是为未知的 URL 返回 404 Not Found,而是转到 index.html

路由器

index.html 中我们加载了两个文件:styles.cssmain.js。我把样式留给你自由发挥。

让我们移动到 main.js。 创建一个包含以下内容的 static/main.js 文件:

import { guard } from './auth.js'
import Router from './router.js'

let currentPage
const disconnect = new CustomEvent('disconnect')
const router = new Router()

router.handle('/', guard(view('home'), view('access')))
router.handle('/callback', view('callback'))
router.handle(/^\/conversations\/([^\/]+)$/, guard(view('conversation'), view('access')))
router.handle(/^\//, view('not-found'))

router.install(async result => {
    document.body.innerHTML = ''
    if (currentPage instanceof Node) {
        currentPage.dispatchEvent(disconnect)
    }
    currentPage = await result
    if (currentPage instanceof Node) {
        document.body.appendChild(currentPage)
    }
})

function view(pageName) {
    return (...args) => import(`/pages/${pageName}-page.js`)
        .then(m => m.default(...args))
}

如果你是这个博客的关注者,你已经知道它是如何工作的了。 该路由器就是在 这里 显示的那个。 只需从 @nicolasparada/router 下载并保存到 static/router.js 即可。

我们注册了四条路由。 在根路由 / 处,我们展示 homeaccess 页面,无论用户是否通过身份验证。 在 /callback 中,我们展示 callback 页面。 在 /conversations/{conversationID} 上,我们展示对话或 access 页面,无论用户是否通过验证,对于其他 URL,我们展示一个 not-found 页面。

我们告诉路由器将结果渲染为文档主体,并在离开之前向每个页面调度一个 disconnect 事件。

我们将每个页面放在不同的文件中,并使用新的动态 import() 函数导入它们。

身份验证

guard() 是一个函数,给它两个函数作为参数,如果用户通过了身份验证,则执行第一个函数,否则执行第二个。它来自 auth.js,所以我们创建一个包含以下内容的 static/auth.js 文件:

export function isAuthenticated() {
    const token = localStorage.getItem('token')
    const expiresAtItem = localStorage.getItem('expires\_at')
    if (token === null || expiresAtItem === null) {
        return false
    }

    const expiresAt = new Date(expiresAtItem)
    if (isNaN(expiresAt.valueOf()) || expiresAt <= new Date()) {
        return false
    }

    return true
}

export function guard(fn1, fn2) {
    return (...args) => isAuthenticated()
        ? fn1(...args)
        : fn2(...args)
}

export function getAuthUser() {
    if (!isAuthenticated()) {
        return null
    }

    const authUser = localStorage.getItem('auth\_user')
    if (authUser === null) {
        return null
    }

    try {
        return JSON.parse(authUser)
    } catch (_) {
        return null
    }
}

isAuthenticated() 检查 localStorage 中的 tokenexpires_at,以判断用户是否已通过身份验证。getAuthUser()localStorage 中获取经过身份验证的用户。

当我们登录时,我们会将所有的数据保存到 localStorage,这样才有意义。

Access 页面

access page screenshot

让我们从 access 页面开始。 创建一个包含以下内容的文件 static/pages/access-page.js

const template = document.createElement('template')
template.innerHTML = `
 <h1>Messenger</h1>
 <a href="/api/oauth/github" onclick="event.stopPropagation()">Access with GitHub</a>
`

export default function accessPage() {
    return template.content
}

因为路由器会拦截所有链接点击来进行导航,所以我们必须特别阻止此链接的事件传播。

单击该链接会将我们重定向到后端,然后重定向到 GitHub,再重定向到后端,然后再次重定向到前端; 到 callback 页面。

Callback 页面

创建包括以下内容的 static/pages/callback-page.js 文件:

import http from '../http.js'
import { navigate } from '../router.js'

export default async function callbackPage() {
    const url = new URL(location.toString())
    const token = url.searchParams.get('token')
    const expiresAt = url.searchParams.get('expires\_at')

    try {
        if (token === null || expiresAt === null) {
            throw new Error('Invalid URL')
        }

        const authUser = await getAuthUser(token)

        localStorage.setItem('auth\_user', JSON.stringify(authUser))
        localStorage.setItem('token', token)
        localStorage.setItem('expires\_at', expiresAt)
    } catch (err) {
        alert(err.message)
    } finally {
        navigate('/', true)
    }
}

function getAuthUser(token) {
    return http.get('/api/auth\_user', { authorization: `Bearer ${token}` })
}

callback 页面不呈现任何内容。这是一个异步函数,它使用 URL 查询字符串中的 token 向 /api/auth_user 发出 GET 请求,并将所有数据保存到 localStorage。 然后重定向到 /

HTTP

这里是一个 HTTP 模块。 创建一个包含以下内容的 static/http.js 文件:

import { isAuthenticated } from './auth.js'

async function handleResponse(res) {
    const body = await res.clone().json().catch(() => res.text())

    if (res.status === 401) {
        localStorage.removeItem('auth\_user')
        localStorage.removeItem('token')
        localStorage.removeItem('expires\_at')
    }

    if (!res.ok) {
        const message = typeof body === 'object' && body !== null && 'message' in body
            ? body.message
            : typeof body === 'string' && body !== ''
                ? body
                : res.statusText
        throw Object.assign(new Error(message), {
            url: res.url,
            statusCode: res.status,
            statusText: res.statusText,
            headers: res.headers,
            body,
        })
    }

    return body
}

function getAuthHeader() {
    return isAuthenticated()
        ? { authorization: `Bearer ${localStorage.getItem('token')}` }
        : {}
}

export default {
    get(url, headers) {
        return fetch(url, {
            headers: Object.assign(getAuthHeader(), headers),
        }).then(handleResponse)
    },

    post(url, body, headers) {
        const init = {
            method: 'POST',
            headers: getAuthHeader(),
        }
        if (typeof body === 'object' && body !== null) {
            init.body = JSON.stringify(body)
            init.headers['content-type'] = 'application/json; charset=utf-8'
        }
        Object.assign(init.headers, headers)
        return fetch(url, init).then(handleResponse)
    },

    subscribe(url, callback) {
        const urlWithToken = new URL(url, location.origin)
        if (isAuthenticated()) {
            urlWithToken.searchParams.set('token', localStorage.getItem('token'))
        }
        const eventSource = new EventSource(urlWithToken.toString())
        eventSource.onmessage = ev => {
            let data
            try {
                data = JSON.parse(ev.data)
            } catch (err) {
                console.error('could not parse message data as JSON:', err)
                return
            }
            callback(data)
        }
        const unsubscribe = () => {
            eventSource.close()
        }
        return unsubscribe
    },
}

这个模块是 fetchEventSource API 的包装器。最重要的部分是它将 JSON web 令牌添加到请求中。

Home 页面

home page screenshot

因此,当用户登录时,将显示 home 页。 创建一个具有以下内容的 static/pages/home-page.js 文件:

import { getAuthUser } from '../auth.js'
import { avatar } from '../shared.js'

export default function homePage() {
    const authUser = getAuthUser()
    const template = document.createElement('template')
    template.innerHTML = `
 <div>
 <div>
 ${avatar(authUser)}
 <span>${authUser.username}</span>
 </div>
 <button id="logout-button">Logout</button>
 </div>
 <!-- conversation form here -->
 <!-- conversation list here -->
 `
    const page = template.content
    page.getElementById('logout-button').onclick = onLogoutClick
    return page
}

function onLogoutClick() {
    localStorage.clear()
    location.reload()
}

对于这篇文章,这是我们在 home 页上呈现的唯一内容。我们显示当前经过身份验证的用户和注销按钮。

当用户单击注销时,我们清除 localStorage 中的所有内容并重新加载页面。

Avatar

那个 avatar() 函数用于显示用户的头像。 由于已在多个地方使用,因此我将它移到 shared.js 文件中。 创建具有以下内容的文件 static/shared.js

export function avatar(user) {
    return user.avatarUrl === null
        ? `<figure class="avatar" data-initial="${user.username[0]}"></figure>`
        : `<img class="avatar" src="${user.avatarUrl}" alt="${user.username}'s avatar">`
}

如果头像网址为 null,我们将使用用户的姓名首字母作为初始头像。

你可以使用 attr() 函数显示带有少量 CSS 样式的首字母。

.avatar[data-initial]::after {
    content: attr(data-initial);
}

仅开发使用的登录

access page with login form screenshot

在上一篇文章中,我们为编写了一个登录代码。让我们在 access 页面中为此添加一个表单。 进入 static/ages/access-page.js,稍微修改一下。

import http from '../http.js'

const template = document.createElement('template')
template.innerHTML = `
 <h1>Messenger</h1>
 <form id="login-form">
 <input type="text" placeholder="Username" required>
 <button>Login</button>
 </form>
 <a href="/api/oauth/github" onclick="event.stopPropagation()">Access with GitHub</a>
`

export default function accessPage() {
    const page = template.content.cloneNode(true)
    page.getElementById('login-form').onsubmit = onLoginSubmit
    return page
}

async function onLoginSubmit(ev) {
    ev.preventDefault()

    const form = ev.currentTarget
    const input = form.querySelector('input')
    const submitButton = form.querySelector('button')

    input.disabled = true
    submitButton.disabled = true

    try {
        const payload = await login(input.value)
        input.value = ''

        localStorage.setItem('auth\_user', JSON.stringify(payload.authUser))
        localStorage.setItem('token', payload.token)
        localStorage.setItem('expires\_at', payload.expiresAt)

        location.reload()
    } catch (err) {
        alert(err.message)
        setTimeout(() => {
            input.focus()
        }, 0)
    } finally {
        input.disabled = false
        submitButton.disabled = false
    }
}

function login(username) {
    return http.post('/api/login', { username })
}

我添加了一个登录表单。当用户提交表单时。它使用用户名对 /api/login 进行 POST 请求。将所有数据保存到 localStorage 并重新加载页面。

记住在前端完成后删除此表单。


这就是这篇文章的全部内容。在下一篇文章中,我们将继续使用主页添加一个表单来开始对话,并显示包含最新对话的列表。


via: https://nicolasparada.netlify.com/posts/go-messenger-access-page/

作者:Nicolás Parada 选题:lujun9972 译者:gxlct008 校对:wxy

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

使用 Ansible 剧本自动安装和更新设备上的软件。

Ansible 是系统管理员和开发人员用来保持计算机系统处于最佳状态的一种流行的自动化工具。与可扩展框架一样,Ansible 本身功能有限,它真正的功能体现在许多模块中。在某种程度上,Ansible 模块就是 Linux 系统的命令。它们针对特定问题提供解决方案,而维护计算机时的一项常见任务是使所有计算机的更新和一致。

我曾经使用软件包的文本列表来保持系统或多或少的同步:我会列出笔记本电脑上安装的软件包,然后将其与台式机或另一台服务器之间进行交叉参考,手动弥补差异。当然,在 Linux 机器上安装和维护应用程序是 Ansible 的一项基本功能,这意味着你可以在自己关心的计算机上列出所需的内容。

寻找正确的 Ansible 模块

Ansible 模块的数量非常庞大,如何找到能完成你任务的模块?在 Linux 中,你可以在应用程序菜单或 /usr/bin 中查找要运行的应用程序。使用 Ansible 时,你可以参考 Ansible 模块索引

这个索引按照类别列出。稍加搜索,你就很可能找到所需的模块。对于包管理,Packaging 模块几乎适用于所有带包管理器的系统。

动手写一个 Ansible 剧本

首先,选择本地计算机上的包管理器。例如,如果你打算在运行 Fedora 的笔记本电脑上编写 Ansible 指令(在 Ansible 中称为“ 剧本 playbook ”),那么从 dnf 模块开始。如果你在 Elementary OS 上编写,使用 apt 模块,以此类推。这样你就可以开始进行测试和验证,并可以在以后扩展到其它计算机。

第一步是创建一个代表你的剧本的目录。这不是绝对必要的,但这是一个好习惯。Ansible 只需要一个配置文件就可以运行在 YAML 中,但是如果你以后想要扩展剧本,你就可以通过改变目录和文件的方式来控制 Ansible。现在,只需创建一个名为 install_packages 或类似的目录:

$ mkdir ~/install_packages

你可以根据自己的喜好来命名 Ansible 的剧本,但通常将其命名为 site.yml

$ touch ~/install_packages/site.yml

在你最喜欢的文本编辑器中打开 site.yml,添加以下内容:

---
- hosts: localhost
  tasks:
    - name: install packages
      become: true
      become_user: root
      dnf:
        state: present
        name:
         - tcsh
         - htop

你必须调整使用的模块名称以匹配你使用的发行版。在此示例中,我使用 dnf 是因为我在 Fedora Linux 上编写剧本。

就像 Linux 终端中的命令一样,知道 如何 来调用 Ansible 模块就已经成功了一半。这个示例剧本遵循标准剧本格式:

  • hosts 是一台或多台计算机。在本示例中,目标计算机是 localhost,即你当前正在使用的计算机(而不是你希望 Ansible 连接的远程系统)。
  • tasks 是你要在主机上执行的任务列表。

    • name 是任务的人性化名称。在这种情况下,我使用 install packages,因为这就是该任务正在做的事情。
    • become 允许 Ansible 更改运行此任务的用户。
    • become_user 允许 Ansible 成为 root 用户来运行此任务。这是必须的,因为只有 root 用户才能使用 dnf 安装应用程序。
    • dnf 是模块名称,你可以在 Ansible 网站上的模块索引中找到。

dnf 下的节点是 dnf 模块专用的。这是模块文档的关键所在。就像 Linux 命令的手册页一样,模块文档会告诉你可用的选项和所需的参数。

 title=

安装软件包是一个相对简单的任务,仅需要两个元素。state 选项指示 Ansible 检查系统上是否存在 软件包,而 name 选项列出要查找的软件包。Ansible 会针对机器的 状态 进行调整,因此模块指令始终意味着更改。假如 Ansible 扫描了系统状态,发现剧本里描述的系统(在本例中,tcshhtop 存在)与实际状态存在冲突,那么 Ansible 的任务是进行必要的更改来使系统与剧本匹配。Ansible 可以通过 dnf(或 apt 或者其它任何包管理器)模块进行更改。

每个模块可能都有一组不同的选项,所以在编写剧本时,要经常参考模块文档。除非你对模块非常熟悉,否则这是期望模块完成工作的唯一合理方法。

验证 YAML

剧本是用 YAML 编写的。因为 YAML 遵循严格的语法,所以安装 yamllint 来检查剧本是很有帮助的。更妙的是,有一个专门针对 Ansible 的检查工具称为 ansible-lint,它专门为剧本而生。在继续之前,安装它。

在 Fedora 或 CentOs 上:

$ sudo dnf ins tall yamllint python3-ansible-lint

在 Debian、Elementary 或 Ubuntu 上,同样的:

$ sudo apt install yamllint ansible-lint

使用 ansible-link 来验证你的剧本。如果你无法使用 ansible-lint,你可以使用 yamllint

$ ansible-lint ~/install_packages/site.yml

成功则不返回任何内容,但如果文件中有错误,则必须先修复它们,然后再继续。复制和粘贴过程中的常见错误包括在最后一行的末尾省略换行符、使用制表符而不是空格来缩进。在文本编辑器中修复它们,重新运行 ansible-llint,重复这个过程,直到 ansible-lintyamllint 没有返回为止。

使用 Ansible 安装一个应用

现在你有了一个可验证的有效剧本,你终于可以在本地计算机上运行它了,因为你碰巧知道该剧本定义的任务需要 root 权限,所以在调用 Ansible 时必须使用 --ask-become-pass 选项,因此系统会提示你输入管理员密码。

开始安装:

$ ansible-playbook --ask-become-pass ~/install_packages/site.yml
BECOME password:
PLAY [localhost] ******************************

TASK [Gathering Facts] ******************************
ok: [localhost]

TASK [install packages] ******************************
ok: [localhost]

PLAY RECAP ******************************
localhost: ok=0 changed=2 unreachable=0 failed=0 [...]

这些命令被执行后,目标系统将处于与剧本中描述的相同的状态。

在远程系统上安装应用程序

通过这么多操作来替换一个简单的命令可能会适得其反,但是 Ansible 的优势是它可以在你的所有系统中实现自动化。你可以使用条件语句使 Ansible 在不同的系统上使用特定的模块,但是现在,假定所有计算机都使用相同的包管理器。

要连接到远程系统,你必须在 /etc/ansible/hosts 文件中定义远程系统,该文件与 Ansible 是一起安装的,所以它已经存在了,但它可能是空的,除了一些解释性注释之外。使用 sudo 在你喜欢的文本编辑器中打开它。

你可以通过其 IP 地址或主机名(只要主机名可以解析)定义主机。例如,如果你已经在 /etc/hosts 中定义了 liavara 并可以成功 ping 通,那么你可以在 /etc/ansible/hosts 中将 liavara 设置为主机。或者,如果你正在运行一个域名服务器或 Avahi 服务器并且可以 pingliavara,那么你就可以在 /etc/ansible/hosts 中定义它。否则,你必须使用它的 IP 地址。

你还必须成功地建立与目标主机的安全 shell(SSH)连接。最简单的方法是使用 ssh-copy-id 命令,但是如果你以前从未与主机建立 SSH 连接,阅读我关于如何创建自动 SSH 连接的文章

一旦你在 /etc/ansible/hosts 文件中输入了主机名或 IP 地址后,你就可以在剧本中更改 hosts 定义:

---
- hosts: all
  tasks:
    - name: install packages
      become: true
      become_user: root
      dnf:
        state: present
        name:
         - tcsh
         - htop

再次运行 ansible-playbook

$ ansible-playbook --ask-become-pass ~/install_packages/site.yml

这次,剧本会在你的远程系统上运行。

如果你添加更多主机,则有许多方法可以过滤哪个主机执行哪个任务。例如,你可以创建主机组(服务器的 webserves,台式机的 workstations等)。

适用于混合环境的 Ansible

到目前为止,我们一直假定 Ansible 配置的所有主机都运行相同的操作系统(都是是使用 dnf 命令进行程序包管理的操作系统)。那么,如果你要管理不同发行版的主机,例如 Ubuntu(使用 apt)或 Arch(使用 pacman),或者其它的操作系统时,该怎么办?

只要目标操作系统具有程序包管理器(MacOs 有 HomebrewWindows 有 Chocolatey),Ansible 就能派上用场。

这就是 Ansible 优势最明显的地方。在 shell 脚本中,你必须检查目标主机上有哪些可用的包管理器,即使使用纯 Python,也必须检查操作系统。Ansible 不仅内置了这些功能,而且还具有在剧本中使用命令结果的机制。你可以使用 action 关键字来执行由 Ansible 事实收集子系统提供的变量定义的任务,而不是使用 dnf 模块。

---
- hosts: all
  tasks:
    - name: install packages
      become: true
      become_user: root
      action: >
       {{ ansible_pkg_mgr }} name=htop,transmission state=present update_cache=yes

action 关键字会加载目标插件。在本例中,它使用了 ansible_pkg_mgr 变量,该变量由 Ansible 在初始 收集信息 期间填充。你不需要告诉 Ansible 收集有关其运行操作系统的事实,所以很容易忽略这一点,但是当你运行一个剧本时,你会在默认输出中看到它:

TASK [Gathering Facts] *****************************************
ok: [localhost]

action 插件使用来自这个探针的信息,使用相关的包管理器命令填充 ansible_pkg_mgr,以安装在 name 参数之后列出的程序包。使用 8 行代码,你可以克服在其它脚本选项中很少允许的复杂跨平台难题。

使用 Ansible

现在是 21 世纪,我们都希望我们的计算机设备能够互联并且相对一致。无论你维护的是两台还是 200 台计算机,你都不必一次又一次地执行相同的维护任务。使用 Ansible 来同步生活中的计算机设备,看看 Ansible 还能为你做些什么。


via: https://opensource.com/article/20/9/install-packages-ansible

作者:Seth Kenlon 选题:lujun9972 译者:MjSeven 校对:wxy

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

Linux 系统上的 lshw 命令提供的系统设备信息比我们大多数人想象的要多得多。

虽然 lshw 命令( 列出硬件 list hardware ,读作 “ls hardware”)远不是每个人最先学会的 50 个 Linux 命令之一,但它可以提供很多系统硬件的有用信息。

它以一种相当易于理解的格式提取出可能比你知道的更多的信息。在看到描述、(设备)逻辑名称、大小等以后,你可能会理解到自己能获得多少信息。

这篇文章会研究 lshw 给出的信息,但侧重于磁盘及相关硬件。下面是 lshw 的输出示例:

$ sudo lshw -C disk
  *-disk:0
       description: SCSI Disk
       product: Card Reader-1
       vendor: JIE LI
       physical id: 0.0.0
       bus info: scsi@4:0.0.0
       logical name: /dev/sdc
       version: 1.00
       capabilities: removable
       configuration: logicalsectorsize=512 sectorsize=512
     *-medium
          physical id: 0
          logical name: /dev/sdc

请注意,你需要使用 sudo 运行 lshw 命令以确保能得到所有可用的信息。

虽然我们在上面的命令中要求了输出“磁盘(disk)”(上面只包含了原始输出里五个条目中的一个),这里的输出却不是一个硬盘,而是读卡器——磁盘的一种。注意系统将这个设备命名为了 /dev/sdc

系统的主磁盘上也有相似的信息:

*-disk
        description: ATA Disk
        product: SSD2SC120G1CS175
        physical id: 0
        bus info: scsi@0:0.0.0
         logical name: /dev/sda         <==这里
        version: 1101
        serial: PNY20150000778410606
        size: 111GiB (120GB)
        capabilities: partitioned partitioned:dos
        configuration: ansiversion=5 logicalsectorsize=512 sectorsize=512 signature=
           f63b5929

这块硬盘是 /dev/sda。这个系统上的硬盘都显示为 ATA 磁盘,ATA 是一种把控制器与盘体集成在一起的磁盘驱动器实现。

要获得“磁盘”类设备的简略列表,可以运行下面这条命令。注意其中有两个设备被列出了两次,所以我们看到的仍然是五个磁盘设备。

$ sudo lshw -short -C disk
H/W path               Device      Class          Description
=============================================================
/0/100/1d/1/1/0.0.0    /dev/sdc    disk           Card Reader-1
/0/100/1d/1/1/0.0.0/0  /dev/sdc    disk
/0/100/1d/1/1/0.0.1    /dev/sdd    disk           2
/0/100/1d/1/1/0.0.1/0  /dev/sdd    disk
/0/100/1f.2/0          /dev/sda    disk           120GB SSD2SC120G1CS175
/0/100/1f.2/1          /dev/cdrom  disk           DVD+-RW GSA-H73N
/0/100/1f.5/0.0.0      /dev/sdb    disk           500GB SAMSUNG HE502HJ

如果你决定要查看系统上的 所有 设备,请坐稳了;你会得到一个包含的东西比你通常认为的“设备”要多得多的列表,下面是一个例子,这是一个“简短(short)”(信息很少)的列表:

$ sudo lshw -short
[sudo] password for shs:
H/W path               Device      Class          Description
=============================================================
                                   system         Inspiron 530s
/0                                 bus            0RY007
/0/0                               memory         128KiB BIOS
/0/4                               processor      Intel(R) Core(TM)2 Duo CPU
/0/4/a                             memory         32KiB L1 cache
/0/4/b                             memory         6MiB L2 cache
/0/24                              memory         6GiB System Memory
/0/24/0                            memory         2GiB DIMM DDR2 Synchronous 667
/0/24/1                            memory         1GiB DIMM DDR2 Synchronous 667
/0/24/2                            memory         2GiB DIMM DDR2 Synchronous 667
/0/24/3                            memory         1GiB DIMM DDR2 Synchronous 667
/0/1                               generic
/0/10                              generic
/0/11                              generic
/0/12                              generic
/0/13                              generic
/0/14                              generic
/0/15                              generic
/0/17                              generic
/0/18                              generic
/0/19                              generic
/0/2                               generic
/0/20                              generic
/0/100                             bridge         82G33/G31/P35/P31 Express DRAM
/0/100/1                           bridge         82G33/G31/P35/P31 Express PCI
/0/100/1/0                         display        Caicos [Radeon HD 6450/7450/84
/0/100/1/0.1                       multimedia     Caicos HDMI Audio [Radeon HD 6
/0/100/19              enp0s25     network        82562V-2 10/100 Network Connec
/0/100/1a                          bus            82801I (ICH9 Family) USB UHCI
/0/100/1a/1            usb3        bus            UHCI Host Controller
/0/100/1a.1                        bus            82801I (ICH9 Family) USB UHCI
/0/100/1a.1/1          usb4        bus            UHCI Host Controller
/0/100/1a.1/1/2                    input          Rock Candy Wireless Keyboard
/0/100/1a.2                        bus            82801I (ICH9 Family) USB UHCI
/0/100/1a.2/1          usb5        bus            UHCI Host Controller
/0/100/1a.2/1/2                    input          USB OPTICAL MOUSE
/0/100/1a.7                        bus            82801I (ICH9 Family) USB2 EHCI
/0/100/1a.7/1          usb1        bus            EHCI Host Controller
/0/100/1b                          multimedia     82801I (ICH9 Family) HD Audio
/0/100/1d                          bus            82801I (ICH9 Family) USB UHCI
/0/100/1d/1            usb6        bus            UHCI Host Controller
/0/100/1d/1/1          scsi4       storage        CD04
/0/100/1d/1/1/0.0.0    /dev/sdc    disk           Card Reader-1
/0/100/1d/1/1/0.0.0/0  /dev/sdc    disk
/0/100/1d/1/1/0.0.1    /dev/sdd    disk           2
/0/100/1d/1/1/0.0.1/0  /dev/sdd    disk
/0/100/1d.1                        bus            82801I (ICH9 Family) USB UHCI
/0/100/1d.1/1          usb7        bus            UHCI Host Controller
/0/100/1d.2                        bus            82801I (ICH9 Family) USB UHCI
/0/100/1d.2/1          usb8        bus            UHCI Host Controller
/0/100/1d.7                        bus            82801I (ICH9 Family) USB2 EHCI
/0/100/1d.7/1          usb2        bus            EHCI Host Controller
/0/100/1d.7/1/2                    multimedia     USB  Live camera
/0/100/1e                          bridge         82801 PCI Bridge
/0/100/1e/1                        communication  HSF 56k Data/Fax Modem
/0/100/1f                          bridge         82801IR (ICH9R) LPC Interface
/0/100/1f.2            scsi0       storage        82801IR/IO/IH (ICH9R/DO/DH) 4
/0/100/1f.2/0          /dev/sda    disk           120GB SSD2SC120G1CS175
/0/100/1f.2/0/1        /dev/sda1   volume         111GiB EXT4 volume
/0/100/1f.2/1          /dev/cdrom  disk           DVD+-RW GSA-H73N
/0/100/1f.3                        bus            82801I (ICH9 Family) SMBus Con
/0/100/1f.5            scsi3       storage        82801I (ICH9 Family) 2 port SA
/0/100/1f.5/0.0.0      /dev/sdb    disk           500GB SAMSUNG HE502HJ
/0/100/1f.5/0.0.0/1    /dev/sdb1   volume         433GiB EXT4 volume
/0/3                               system         PnP device PNP0c02
/0/5                               system         PnP device PNP0b00
/0/6                               storage        PnP device PNP0700
/0/7                               system         PnP device PNP0c02
/0/8                               system         PnP device PNP0c02
/0/9                               system         PnP device PNP0c01

运行下面的命令来列出设备类别,并统计每个类别中的设备数量。

$ sudo lshw -short | awk ‘{print substr($0,36,13)}’ | tail -n +3 | sort | uniq -c
      4 bridge
     18 bus
      1 communication
      7 disk
      1 display
     12 generic
      2 input
      8 memory
      3 multimedia
      1 network
      1 processor
      4 storage
      6 system
      2 volume

注意: 上面使用 awk 命令从 lshw 的输出中选择 Class(类别)栏是这样实现的:使用 $0(选取完整行),但只取从正确位置(第 36 个字符)开始的子串,而因为“类别”中并没有条目的长度超过 13 个字符,所以子串就在那里结束。命令中 tail -n +3 的部分移除了标题和下面的=====,所以最终的列表中只包含了那 14 种设备类型。

(LCTT 译注:上面的命令中 awk 的部分在选取子串时是从第 36 个字符开始的,这个数字基本上取决于最长的设备逻辑名称的长度,因而在不同的系统环境中可能有所不同,一个例子是,当你的系统上有 NVMe SSD 时,可能需要将其改为 41。)

你会发现在没有使用 -short 选项的时候,每一个磁盘类设备都会有大约 12 行的输出,包括像是 /dev/sda 这样的逻辑名称,磁盘大小和种类等等。

$ sudo lshw -C disk
[sudo] password for shs:
  *-disk:0
       description: SCSI Disk
       product: Card Reader-1           <== 读卡器? 
       vendor: JIE LI
       physical id: 0.0.0
       bus info: scsi@4:0.0.0
       logical name: /dev/sdc
       version: 1.00
       capabilities: removable
       configuration: logicalsectorsize=512 sectorsize=512
     *-medium
          physical id: 0
          logical name: /dev/sdc
  *-disk:1
       description: SCSI Disk
       product: 2
       vendor: AC4100 -
       physical id: 0.0.1
       bus info: scsi@4:0.0.1
       logical name: /dev/sdd
       capabilities: removable
       configuration: logicalsectorsize=512 sectorsize=512
     *-medium
          physical id: 0
          logical name: /dev/sdd
  *-disk
       description: ATA Disk
       product: SSD2SC120G1CS175
       physical id: 0
       bus info: scsi@0:0.0.0
       logical name: /dev/sda           <== 主要磁盘
       version: 1101
       serial: PNY20150000778410606
       size: 111GiB (120GB)
       capabilities: partitioned partitioned:dos
       configuration: ansiversion=5 logicalsectorsize=512 sectorsize=512 signature=f63b5929
  *-cdrom                               <== 也叫 /dev/sr0
       description: DVD writer
       product: DVD+-RW GSA-H73N
       vendor: HL-DT-ST
       physical id: 1
       bus info: scsi@1:0.0.0
       logical name: /dev/cdrom
       logical name: /dev/cdrw
       logical name: /dev/dvd
       logical name: /dev/dvdrw
       logical name: /dev/sr0
       version: B103
       serial: [
       capabilities: removable audio cd-r cd-rw dvd dvd-r
       configuration: ansiversion=5 status=nodisc
  *-disk
       description: ATA Disk
       product: SAMSUNG HE502HJ
       physical id: 0.0.0
       bus info: scsi@3:0.0.0
       logical name: /dev/sdb           <== 次要磁盘
       version: 0002
       serial: S2B6J90B501053
       size: 465GiB (500GB)
       capabilities: partitioned partitioned:dos
       configuration: ansiversion=5 logicalsectorsize=512 sectorsize=512 signature=7e67ccf3

总结

lshw 命令提供了一些我们许多人通常不会处理的信息,不过即使你只用了其中的一部分,知道有多少信息可用还是很不错的。


via: https://www.networkworld.com/article/3583598/how-to-view-information-on-your-linux-devices-with-lshw.html

作者:Sandra Henry-Stocker 选题:lujun9972 译者:rakino 校对:wxy

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

SEODeploy 可以帮助我们在网站部署之前识别出 SEO 问题。

作为一个技术性搜索引擎优化开发者,我经常被请来协助做网站迁移、新网站发布、分析实施和其他一些影响网站在线可见性和测量等领域,以控制风险。许多公司每月经常性收入的很大一部分来自用户通过搜索引擎找到他们的产品和服务。虽然搜索引擎已经能妥善地处理没有被良好格式化的代码,但在开发过程中还是会出问题,对搜索引擎如何索引和为用户显示页面产生不利影响。

我曾经也尝试通过评审各阶段会破坏 SEO( 搜索引擎优化 search engine optimization )的问题来手动降低这种风险。我的团队最终审查到的结果,决定了该项目是否可以上线。但这个过程通常很低效,只能用于有限的页面,而且很有可能出现人为错误。

长期以来,这个行业一直在寻找可用且值得信赖的方式来自动化这一过程,同时还能让开发人员和搜索引擎优化人员在必须测试的内容上获得有意义的发言权。这是非常重要的,因为这些团队在开发冲刺中优先级通常会发生冲突,搜索引擎优化者需要推动变化,而开发人员需要控制退化和预期之外的情况。

常见的破坏 SEO 的问题

我合作过的很多网站有成千上万的页面,甚至上百万。实在令人费解,为什么一个开发过程中的改动能影响这么多页面。在 SEO 的世界中,Google 或其他搜索引擎展示你的页面时,一个非常微小和看起来无关紧要的修改也可能导致全网站范围的变化。在部署到生产环境之前,必须要处理这类错误。

下面是我去年见过的几个例子。

偶发的 noindex

在部署到生产环境之后,我们用的一个专用的第三方 SEO 监控工具 ContentKing 马上发现了这个问题。这个错误很隐蔽,因为它在 HTML 中是不可见的,确切地说,它隐藏在服务器响应头里,但它能很快导致搜索不可见。

HTTP/1.1 200 OK
Date: Tue May 25 2010 21:12:42 GMT
[...]
X-Robots-Tag: noindex
[...]

canonical 小写

上线时错误地把整个网站的 canonical 链接元素全改成小写了。这个改动影响了接近 30000 个 URL。在修改之前,所有的 URL 大小写都正常(例如 URL-Path 这样)。这之所以是个问题是因为 canonical 链接元素是用来给 Google 提示一个网页真实的规范 URL 版本的。这个改动导致很多 URL 被从 Google 的索引中移除并用小写的版本(/url-path)重新建立索引。影响范围是流量损失了 10% 到 15%,也污染了未来几个星期的网页监控数据。

源站退化

有个网站的 React 实现复杂而奇特,它有个神奇的问题,origin.domain.com URL 退化显示为 CDN 服务器的源站。它会在网站元数据(如 canonical 链接元素、URL 和 Open Graph 链接)中间歇性地显示原始的主机而不是 CDN 边缘主机。这个问题在原始的 HTML 和渲染后的 HTML 中都存在。这个问题影响搜索的可见性和在社交媒体上的分享质量。

SEODeploy 介绍

SEO 通常使用差异测试工具来检测渲染后和原始的 HTML 的差异。差异测试是很理想的,因为它避免了肉眼测试的不确定性。你希望检查 Google 对你的页面的渲染过程的差异,而不是检查用户对你页面的渲染。你希望查看下原始的 HTML 是什么样的,而不是渲染后的 HTML,因为 Google 的渲染过程是有独立的两个阶段的。

这促使我和我的同事创造了 SEODeploy 这个“在部署流水线中用于自动化 SEO 测试的 Python 库。”我们的使命是:

开发一个工具,让开发者能提供若干 URL 路径,并允许这些 URL 在生产环境和预演环境的主机上进行差异测试,尤其是对 SEO 相关数据的非预期的退化。

SEODeploy 的机制很简单:提供一个每行内容都是 URL 路径的文本文件,SEODeploy 对那些路径运行一系列模块,对比 生产环境 production 预演环境 staging 的 URL,把检测到的所有的错误和改动信息报告出来。

 title=

这个工具及其模块可以用一个 YAML 文件来配置,可以根据预期的变化进行定制。

 title=

最初的发布版本包含下面的的核心功能和概念:

  1. 开源:我们坚信分享代码可以被大家批评、改进、扩展、分享和复用。
  2. 模块化:Web 开发中有许多不同的堆栈和边缘案例。SEODeploy 工具在概念上很简单,因此采用模块化用来控制复杂性。我们提供了两个建好的模块和一个实例模块来简述基本结构。
  3. URL 抽样:由于它不是对所有 URL 都是可行和有效的,因此我们引入了一种随机抽取 XML 网站地图 URL 或被 ContentKing 监控的 URL 作为样本的方法。
  4. 灵活的差异检测:Web 数据是凌乱的。无论被检测的数据是什么类型(如 ext、数组或列表、JSON 对象或字典、整数、浮点数等等),差异检测功能都会尝试将这些数据转换为差异信息。
  5. 自动化: 你可以在命令行来调用抽样和运行方法,将 SEODeploy 融合到已有的流水线也很简单。

模块

虽然核心功能很简单,但在设计上,SEODeploy 的强大功能和复杂度体现在模块上。模块用来处理更难的任务:获取、清理和组织预演服务器和生产服务器上的数据来作对比。

Headless 模块

Headless 模块 是为那些从库里获取数据时不想为第三方服务付费的开发者准备的。它可以运行任意版本的 Chrome,会从每组用来比较的 URL 中提取渲染的数据。

Headless 模块会提取下面的核心数据用来比较:

  1. SEO 内容,如标题、H1-H6、链接等等。
  2. 从 Chrome 计时器 Timings 和 CDP( Chrome 开发工具协议 Chrome DevTools Protocol )性能 API 中提取性能数据
  3. 计算出的性能指标,包括 CLS( 累积布局偏移 Cumulative Layout Shift ),这是 Google 最近发布的一个很受欢迎的 Web 核心数据
  4. 从上述 CDP 的覆盖率 API 获取的 CSS 和 JavaScript 的覆盖率数据

这个模块引入了处理预演环境、网络速度预设(为了让对比更规范化)等功能,也引入了一个处理在预演对比数据中替换预演主机的方法。开发者也能很容易地扩展这个模块,以收集他们想要在每个页面上进行比较的任何其他数据。

其他模块

我们为开发者创建了一个示例模块,开发者可以参照它来使用框架创建一个自定义的提取模块。另一个示例模块是与 ContentKing 结合的。ContentKing 模块需要有 ContentKing 订阅,而 Headless 可以在所有能运行 Chrome 的机器上运行。

需要解决的问题

我们有扩展和强化工具库的计划,但正在寻求开发人员的反馈,了解哪些是可行的,哪些是不符合他们的需求。我们正在解决的问题和条目有:

  1. 对于某些对比元素(尤其是 schema),动态时间戳会产生误报。
  2. 把测试数据保存到数据库,以便查看部署历史以及与上次的预演推送进行差异测试。
  3. 通过云基础设施的渲染,强化提取的规模和速度。
  4. 把测试覆盖率从现在的 46% 提高到 99% 以上。
  5. 目前,我们依赖 Poetry 进行部署管理,但我们希望发布一个 PyPl 库,这样就可以用 pip install 轻松安装。
  6. 我们还在关注更多使用时的问题和相关数据。

开始使用

这个项目在 GitHub 上,我们对大部分功能都提供了 文档

我们希望你能克隆 SEODeploy 并试试它。我们的目标是通过这个由技术性搜索引擎优化开发者开发的、经过开发者和工程师们验证的工具来支持开源社区。我们都见过验证复杂的预演问题需要多长时间,也都见过大量 URL 的微小改动能有什么样的业务影响。我们认为这个库可以为开发团队节省时间、降低部署过程中的风险。

如果你有问题或者想提交代码,请查看项目的关于页面。


via: https://opensource.com/article/20/7/seodeploy

作者:JR Oakes 选题:lujun9972 译者:lxbwolf 校对:wxy

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

制作模板可以让你更快地开始写作新的文档。

我只是偶然发现了 GNOME 的一个新功能(对我来说是的):创建文档模版。 模版 template 也被称作 样版文件 boilerplate ,一般是有着特定格式的空文档,例如律师事务所的信笺,在其顶部有着律所的名称和地址;另一个例子是银行以及保险公司的保函,在其底部页脚包含着某些免责声明。由于这类信息很少改变,你可以把它们添加到空文档中作为模板使用。

一天,在浏览我的 Linux 系统文件的时候,我点击了 模板 Templates 文件夹,然后刚好发现窗口的上方有一条消息写着:“将文件放入此文件夹并用作新文档的模板”,以及一个“获取详情……” 的链接,打开了模板的 GNOME 帮助页面

 title=

创建模板

在 GNOME 中创建模板非常简单。有几种方法可以把文件放进模板文件夹里:你既可以通过图形用户界面(GUI)或是命令行界面(CLI)从另一个位置复制或移动文件,也可以创建一个全新的文件;我选择了后者,实际上,我创建了两个文件。

 title=

我的第一份模板是为 Opensource.com 的文章准备的,它有一个输入标题的位置以及关于我的名字和文章使用的许可证的几行。我的文章使用 Markdown 格式,所以我将模板创建为了一个新的 Markdown 文档——Opensource.com Article.md

# Title    

An article for Opensource.com
by: Alan Formy-Duval
Creative Commons BY-SA 4.0

我将这份文档保存在了 /home/alan/Templates 文件夹内,现在 GNOME 就可以将这个文件识别为模板,并在我要创建新文档的时候提供建议了。

使用模板

每当我有了新文章的灵感的时候,我只需要在我计划用来组织内容的文件夹里单击右键,然后从 新建文档 New Document 列表中选择我想要的模板就可以开始了。

 title=

你可以为各种文档或文件制作模板。我写这篇文章时使用了我为 Opensource.com 的文章创建的模板。程序员可能会把模板用于软件代码,这样的话也许你想要只包含 main() 的模板。

GNOME 桌面环境为 Linux 及相关操作系统的用户提供了一个非常实用、功能丰富的界面。你最喜欢的 GNOME 功能是什么,你又是怎样使用它们的呢?请在评论中分享~


via: https://opensource.com/article/20/9/gnome-templates

作者:Alan Formy-Duval 选题:lujun9972 译者:rakino 校对:wxy

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