2018年10月

我们将会在本篇文章中看到从零开始实现的编译器,将简单的类 LISP 计算语言编译成 JavaScript。完整的源代码在 这里

我们将会:

  1. 自定义语言,并用它编写一个简单的程序
  2. 实现一个简单的解析器组合器
  3. 为该语言实现一个解析器
  4. 为该语言实现一个美观的打印器
  5. 为我们的用途定义 JavaScript 的一个子集
  6. 实现代码转译器,将代码转译成我们定义的 JavaScript 子集
  7. 把所有东西整合在一起

开始吧!

1、定义语言

Lisp 族语言最迷人的地方在于,它们的语法就是树状表示的,这就是这门语言很容易解析的原因。我们很快就能接触到它。但首先让我们把自己的语言定义好。关于我们语言的语法的范式(BNF)描述如下:

program ::= expr
expr ::= <integer> | <name> | ([<expr>])

基本上,我们可以在该语言的最顶层定义表达式并对其进行运算。表达式由一个整数(比如 5)、一个变量(比如 x)或者一个表达式列表(比如 (add x 1))组成。

整数对应它本身的值,变量对应它在当前环境中绑定的值,表达式列表对应一个函数调用,该列表的第一个参数是相应的函数,剩下的表达式是传递给这个函数的参数。

该语言中,我们保留一些内建的特殊形式,这样我们就能做一些更有意思的事情:

  • let 表达式使我们可以在它的 body 环境中引入新的变量。语法如下:
let ::= (let ([<letarg>]) <body>)
letargs ::= (<name> <expr>)
body ::= <expr>
  • lambda 表达式:也就是匿名函数定义。语法如下:
lambda ::= (lambda ([<name>]) <body>)

还有一些内建函数: addmulsubdivprint

让我们看看用我们这门语言编写的入门示例程序:

(let
  ((compose
    (lambda (f g)
      (lambda (x) (f (g x)))))
  (square
    (lambda (x) (mul x x)))
  (add1
    (lambda (x) (add x 1))))
  (print ((compose square add1) 5)))

这个程序定义了 3 个函数:composesquareadd1。然后将计算结果的值 ((compose square add1) 5) 输出出来。

我相信了解这门语言,这些信息就足够了。开始实现它吧。

在 Haskell 中,我们可以这样定义语言:

type Name = String

data Expr
  = ATOM Atom
  | LIST [Expr]
    deriving (Eq, Read, Show)

data Atom
  = Int Int
  | Symbol Name
    deriving (Eq, Read, Show)

我们可以解析用该语言用 Expr 定义的程序。而且,这里我们添加了新数据类型 EqReadShow 等实例用于测试和调试。你能够在 REPL 中使用这些数据类型,验证它们确实有用。

我们不在语法中定义 lambdalet 或其它的内建函数,原因在于,当前情况下我们没必要用到这些东西。这些函数仅仅是 LIST (表达式列表)的更加特殊的用例。所以我决定将它放到后面的部分。

一般来说你想要在抽象语法中定义这些特殊用例 —— 用于改进错误信息、禁用静态分析和优化等等,但在这里我们不会这样做,对我们来说这些已经足够了。

另一件你想做的事情可能是在语法中添加一些注释信息。比如定位:Expr 是来自哪个文件的,具体到这个文件的哪一行哪一列。你可以在后面的阶段中使用这一特性,打印出错误定位,即使它们不是处于解析阶段。

  • 练习 1:添加一个 Program 数据类型,可以按顺序包含多个 Expr
  • 练习 2:向语法树中添加一个定位注解。

2、实现一个简单的解析器组合库

我们要做的第一件事情是定义一个 嵌入式领域专用语言 Embedded Domain Specific Language (EDSL),我们会用它来定义我们的语言解析器。这常常被称为解析器组合库。我们做这件事完全是出于学习的目的,Haskell 里有很好的解析库,在实际构建软件或者进行实验时,你应该使用它们。megaparsec 就是这样的一个库。

首先我们来谈谈解析库的实现的思路。本质上,我们的解析器就是一个函数,接受一些输入,可能会读取输入的一些或全部内容,然后返回解析出来的值和无法解析的输入部分,或者在解析失败时抛出异常。我们把它写出来。

newtype Parser a
  = Parser (ParseString -> Either ParseError (a, ParseString))

data ParseString
  = ParseString Name (Int, Int) String

data ParseError
  = ParseError ParseString Error

type Error = String

这里我们定义了三个主要的新类型。

第一个,Parser a 是之前讨论的解析函数。

第二个,ParseString 是我们的输入或携带的状态。它有三个重要的部分:

  • Name: 这是源的名字
  • (Int, Int): 这是源的当前位置
  • String: 这是等待解析的字符串

第三个,ParseError 包含了解析器的当前状态和一个错误信息。

现在我们想让这个解析器更灵活,我们将会定义一些常用类型的实例。这些实例让我们能够将小巧的解析器和复杂的解析器结合在一起(因此它的名字叫做 “解析器组合器”)。

第一个是 Functor 实例。我们需要 Functor 实例,因为我们要能够对解析值应用函数从而使用不同的解析器。当我们定义自己语言的解析器时,我们将会看到关于它的示例。

instance Functor Parser where
  fmap f (Parser parser) =
    Parser (\str -> first f <$> parser str)

第二个是 Applicative 实例。该实例的常见用例是在多个解析器中实现一个纯函数。

instance Applicative Parser where
  pure x = Parser (\str -> Right (x, str))
  (Parser p1) <*> (Parser p2) =
    Parser $
      \str -> do
        (f, rest)  <- p1 str
        (x, rest') <- p2 rest
        pure (f x, rest')

(注意:我们还会实现一个 Monad 实例,这样我们才能使用符号)

第三个是 Alternative 实例。万一前面的解析器解析失败了,我们要能够提供一个备用的解析器。

instance Alternative Parser where
  empty = Parser (`throwErr` "Failed consuming input")
  (Parser p1) <|> (Parser p2) =
    Parser $
      \pstr -> case p1 pstr of
        Right result -> Right result
        Left  _      -> p2 pstr

第四个是 Monad 实例。这样我们就能链接解析器。

instance Monad Parser where
  (Parser p1) >>= f =
    Parser $
     \str -> case p1 str of
       Left err -> Left err
       Right (rs, rest) ->
         case f rs of
           Parser parser -> parser rest

接下来,让我们定义一种的方式,用于运行解析器和防止失败的助手函数:

runParser :: String -> String -> Parser a -> Either ParseError (a, ParseString)
runParser name str (Parser parser) = parser $ ParseString name (0,0) str

throwErr :: ParseString -> String -> Either ParseError a
throwErr ps@(ParseString name (row,col) _) errMsg =
  Left $ ParseError ps $ unlines
    [ "*** " ++ name ++ ": " ++ errMsg
    , "* On row " ++ show row ++ ", column " ++ show col ++ "."
    ]

现在我们将会开始实现组合器,这是 EDSL 的 API,也是它的核心。

首先,我们会定义 oneOf。如果输入列表中的字符后面还有字符的话,oneOf 将会成功,否则就会失败。

oneOf :: [Char] -> Parser Char
oneOf chars =
  Parser $ \case
    ps@(ParseString name (row, col) str) ->
      case str of
        []     -> throwErr ps "Cannot read character of empty string"
        (c:cs) ->
          if c `elem` chars
          then Right (c, ParseString name (row, col+1) cs)
          else throwErr ps $ unlines ["Unexpected character " ++ [c], "Expecting one of: " ++ show chars]

optional 将会抛出异常,停止解析器。失败时它仅仅会返回 Nothing

optional :: Parser a -> Parser (Maybe a)
optional (Parser parser) =
  Parser $
    \pstr -> case parser pstr of
      Left _ -> Right (Nothing, pstr)
      Right (x, rest) -> Right (Just x, rest)

many 将会试着重复运行解析器,直到失败。当它完成的时候,会返回成功运行的解析器列表。many1 做的事情是一样的,但解析失败时它至少会抛出一次异常。

many :: Parser a -> Parser [a]
many parser = go []
  where go cs = (parser >>= \c -> go (c:cs)) <|> pure (reverse cs)

many1 :: Parser a -> Parser [a]
many1 parser =
  (:) <$> parser <*> many parser

下面的这些解析器通过我们定义的组合器来实现一些特殊的解析器:

char :: Char -> Parser Char
char c = oneOf [c]

string :: String -> Parser String
string = traverse char

space :: Parser Char
space = oneOf " \n"

spaces :: Parser String
spaces = many space

spaces1 :: Parser String
spaces1 = many1 space

withSpaces :: Parser a -> Parser a
withSpaces parser =
  spaces *> parser <* spaces

parens :: Parser a -> Parser a
parens parser =
     (withSpaces $ char '(')
  *> withSpaces parser
  <* (spaces *> char ')')

sepBy :: Parser a -> Parser b -> Parser [b]
sepBy sep parser = do
  frst <- optional parser
  rest <- many (sep *> parser)
  pure $ maybe rest (:rest) frst

现在为该门语言定义解析器所需要的所有东西都有了。

  • 练习 :实现一个 EOF(end of file/input,即文件或输入终止符)解析器组合器。

3、为我们的语言实现解析器

我们会用自顶而下的方法定义解析器。

parseExpr :: Parser Expr
parseExpr = fmap ATOM parseAtom <|> fmap LIST parseList

parseList :: Parser [Expr]
parseList = parens $ sepBy spaces1 parseExpr

parseAtom :: Parser Atom
parseAtom = parseSymbol <|> parseInt

parseSymbol :: Parser Atom
parseSymbol = fmap Symbol parseName

注意到这四个函数是在我们这门语言中属于高阶描述。这解释了为什么 Haskell 执行解析工作这么棒。在定义完高级部分后,我们还需要定义低级别的 parseNameparseInt

我们能在这门语言中用什么字符作为名字呢?用小写的字母、数字和下划线吧,而且名字的第一个字符必须是字母。

parseName :: Parser Name
parseName = do
  c  <- oneOf ['a'..'z']
  cs <- many $ oneOf $ ['a'..'z'] ++ "0123456789" ++ "_"
  pure (c:cs)

整数是一系列数字,数字前面可能有负号 -

parseInt :: Parser Atom
parseInt = do
  sign <- optional $ char '-'
  num  <- many1 $ oneOf "0123456789"
  let result = read $ maybe num (:num) sign of
  pure $ Int result

最后,我们会定义用来运行解析器的函数,返回值可能是一个 Expr 或者是一条错误信息。

runExprParser :: Name -> String -> Either String Expr
runExprParser name str =
  case runParser name str (withSpaces parseExpr) of
    Left (ParseError _ errMsg) -> Left errMsg
    Right (result, _) -> Right result
  • 练习 1 :为第一节中定义的 Program 类型编写一个解析器
  • 练习 2 :用 Applicative 的形式重写 parseName
  • 练习 3 :parseInt 可能出现溢出情况,找到处理它的方法,不要用 read

4、为这门语言实现一个更好看的输出器

我们还想做一件事,将我们的程序以源代码的形式打印出来。这对完善错误信息很有用。

printExpr :: Expr -> String
printExpr = printExpr' False 0

printAtom :: Atom -> String
printAtom = \case
  Symbol s -> s
  Int i -> show i

printExpr' :: Bool -> Int -> Expr -> String
printExpr' doindent level = \case
  ATOM a -> indent (bool 0 level doindent) (printAtom a)
  LIST (e:es) ->
    indent (bool 0 level doindent) $
      concat
        [ "("
        , printExpr' False (level + 1) e
        , bool "\n" "" (null es)
        , intercalate "\n" $ map (printExpr' True (level + 1)) es
        , ")"
        ]

indent :: Int -> String -> String
indent tabs e = concat (replicate tabs "  ") ++ e
  • 练习 :为第一节中定义的 Program 类型编写一个美观的输出器

好,目前为止我们写了近 200 行代码,这些代码一般叫做编译器的前端。我们还要写大概 150 行代码,用来执行三个额外的任务:我们需要根据需求定义一个 JS 的子集,定义一个将我们的语言转译成这个子集的转译器,最后把所有东西整合在一起。开始吧。

5、根据需求定义 JavaScript 的子集

首先,我们要定义将要使用的 JavaScript 的子集:

data JSExpr
  = JSInt Int
  | JSSymbol Name
  | JSBinOp JSBinOp JSExpr JSExpr
  | JSLambda [Name] JSExpr
  | JSFunCall JSExpr [JSExpr]
  | JSReturn JSExpr
    deriving (Eq, Show, Read)

type JSBinOp = String

这个数据类型表示 JavaScript 表达式。我们有两个原子类型 JSIntJSSymbol,它们是由我们这个语言中的 Atom 转译来的,我们用 JSBinOp 来表示二元操作,比如 +*,用 JSLambda 来表示匿名函数,和我们语言中的 lambda expression(lambda 表达式) 一样,我们将会用 JSFunCall 来调用函数,用 let 来引入新名字,用 JSReturn 从函数中返回值,在 JavaScript 中是需要返回值的。

JSExpr 类型是对 JavaScript 表达式的 抽象表示。我们会把自己语言中表达式的抽象表示 Expr 转译成 JavaScript 表达式的抽象表示 JSExpr。但为了实现这个功能,我们需要实现 JSExpr ,并从这个抽象表示中生成 JavaScript 代码。我们将通过递归匹配 JSExpr 实现,将 JS 代码当作 String 来输出。这和我们在 printExpr 中做的基本上是一样的。我们还会追踪元素的作用域,这样我们才可以用合适的方式缩进生成的代码。

printJSOp :: JSBinOp -> String
printJSOp op = op

printJSExpr :: Bool -> Int -> JSExpr -> String
printJSExpr doindent tabs = \case
  JSInt    i     -> show i
  JSSymbol name  -> name
  JSLambda vars expr -> (if doindent then indent tabs else id) $ unlines
    ["function(" ++ intercalate ", " vars ++ ") {"
    ,indent (tabs+1) $ printJSExpr False (tabs+1) expr
    ] ++ indent tabs "}"
  JSBinOp  op e1 e2  -> "(" ++ printJSExpr False tabs e1 ++ " " ++ printJSOp op ++ " " ++ printJSExpr False tabs e2 ++ ")"
  JSFunCall f exprs  -> "(" ++ printJSExpr False tabs f ++ ")(" ++ intercalate ", " (fmap (printJSExpr False tabs) exprs) ++ ")"
  JSReturn expr      -> (if doindent then indent tabs else id) $ "return " ++ printJSExpr False tabs expr ++ ";"
  • 练习 1 :添加 JSProgram 类型,它可以包含多个 JSExpr ,然后创建一个叫做 printJSExprProgram 的函数来生成代码。
  • 练习 2 :添加 JSExpr 的新类型:JSIf,并为其生成代码。

6、实现到我们定义的 JavaScript 子集的代码转译器

我们快做完了。这一节将会创建函数,将 Expr 转译成 JSExpr

基本思想很简单,我们会将 ATOM 转译成 JSSymbol 或者 JSInt,然后会将 LIST 转译成一个函数调用或者转译的特例。

type TransError = String

translateToJS :: Expr -> Either TransError JSExpr
translateToJS = \case
  ATOM (Symbol s) -> pure $ JSSymbol s
  ATOM (Int i)    -> pure $ JSInt i
  LIST xs -> translateList xs

translateList :: [Expr] -> Either TransError JSExpr
translateList = \case
  []     -> Left "translating empty list"
  ATOM (Symbol s):xs
    | Just f <- lookup s builtins ->
      f xs
  f:xs ->
    JSFunCall <$> translateToJS f <*> traverse translateToJS xs

builtins 是一系列要转译的特例,就像 lambadalet。每一种情况都可以获得一系列参数,验证它是否合乎语法规范,然后将其转译成等效的 JSExpr

type Builtin  = [Expr] -> Either TransError JSExpr
type Builtins = [(Name, Builtin)]

builtins :: Builtins
builtins =
  [("lambda", transLambda)
  ,("let", transLet)
  ,("add", transBinOp "add" "+")
  ,("mul", transBinOp "mul" "*")
  ,("sub", transBinOp "sub" "-")
  ,("div", transBinOp "div" "/")
  ,("print", transPrint)
  ]

我们这种情况,会将内建的特殊形式当作特殊的、非第一类的进行对待,因此不可能将它们当作第一类函数。

我们会把 Lambda 表达式转译成一个匿名函数:

transLambda :: [Expr] -> Either TransError JSExpr
transLambda = \case
  [LIST vars, body] -> do
    vars' <- traverse fromSymbol vars
    JSLambda vars' <$> (JSReturn <$> translateToJS body)

  vars ->
    Left $ unlines
      ["Syntax error: unexpected arguments for lambda."
      ,"expecting 2 arguments, the first is the list of vars and the second is the body of the lambda."
      ,"In expression: " ++ show (LIST $ ATOM (Symbol "lambda") : vars)
      ]

fromSymbol :: Expr -> Either String Name
fromSymbol (ATOM (Symbol s)) = Right s
fromSymbol e = Left $ "cannot bind value to non symbol type: " ++ show e

我们会将 let 转译成带有相关名字参数的函数定义,然后带上参数调用函数,因此会在这一作用域中引入变量:

transLet :: [Expr] -> Either TransError JSExpr
transLet = \case
  [LIST binds, body] -> do
    (vars, vals) <- letParams binds
    vars' <- traverse fromSymbol vars
    JSFunCall . JSLambda vars' <$> (JSReturn <$> translateToJS body) <*> traverse translateToJS vals
   where
    letParams :: [Expr] -> Either Error ([Expr],[Expr])
    letParams = \case
      [] -> pure ([],[])
      LIST [x,y] : rest -> ((x:) *** (y:)) <$> letParams rest
      x : _ -> Left ("Unexpected argument in let list in expression:\n" ++ printExpr x)

  vars ->
    Left $ unlines
      ["Syntax error: unexpected arguments for let."
      ,"expecting 2 arguments, the first is the list of var/val pairs and the second is the let body."
      ,"In expression:\n" ++ printExpr (LIST $ ATOM (Symbol "let") : vars)
      ]

我们会将可以在多个参数之间执行的操作符转译成一系列二元操作符。比如:(add 1 2 3) 将会变成 1 + (2 + 3)

transBinOp :: Name -> Name -> [Expr] -> Either TransError JSExpr
transBinOp f _ []   = Left $ "Syntax error: '" ++ f ++ "' expected at least 1 argument, got: 0"
transBinOp _ _ [x]  = translateToJS x
transBinOp _ f list = foldl1 (JSBinOp f) <$> traverse translateToJS list

然后我们会将 print 转换成对 console.log 的调用。

transPrint :: [Expr] -> Either TransError JSExpr
transPrint [expr] = JSFunCall (JSSymbol "console.log") . (:[]) <$> translateToJS expr
transPrint xs     = Left $ "Syntax error. print expected 1 arguments, got: " ++ show (length xs)

注意,如果我们将这些代码当作 Expr 的特例进行解析,那我们就可能会跳过语法验证。

  • 练习 1 :将 Program 转译成 JSProgram
  • 练习 2 :为 if Expr Expr Expr 添加一个特例,并将它转译成你在上一次练习中实现的 JSIf 条件语句。

7、把所有东西整合到一起

最终,我们将会把所有东西整合到一起。我们会:

  1. 读取文件
  2. 将文件解析成 Expr
  3. 将文件转译成 JSExpr
  4. 将 JavaScript 代码发送到标准输出流

我们还会启用一些用于测试的标志位:

  • --e 将进行解析并打印出表达式的抽象表示(Expr
  • --pp 将进行解析,美化输出
  • --jse 将进行解析、转译、并打印出生成的 JS 表达式(JSExpr)的抽象表示
  • --ppc 将进行解析,美化输出并进行编译
main :: IO ()
main = getArgs >>= \case
  [file] ->
    printCompile =<< readFile file
  ["--e",file] ->
    either putStrLn print . runExprParser "--e" =<< readFile file
  ["--pp",file] ->
    either putStrLn (putStrLn . printExpr) . runExprParser "--pp" =<< readFile file
  ["--jse",file] ->
    either print (either putStrLn print . translateToJS) . runExprParser "--jse" =<< readFile file
  ["--ppc",file] ->
    either putStrLn (either putStrLn putStrLn) . fmap (compile . printExpr) . runExprParser "--ppc" =<< readFile file
  _ ->
    putStrLn $ unlines
      ["Usage: runghc Main.hs [ --e, --pp, --jse, --ppc ] <filename>"
      ,"--e     print the Expr"
      ,"--pp    pretty print Expr"
      ,"--jse   print the JSExpr"
      ,"--ppc   pretty print Expr and then compile"
      ]

printCompile :: String -> IO ()
printCompile = either putStrLn putStrLn . compile

compile :: String -> Either Error String
compile str = printJSExpr False 0 <$> (translateToJS =<< runExprParser "compile" str)

大功告成。将自己的语言编译到 JS 子集的编译器已经完成了。再说一次,你可以在 这里 看到完整的源文件。

用我们的编译器运行第一节的示例,产生的 JavaScript 代码如下:

$ runhaskell Lisp.hs example.lsp
(function(compose, square, add1) {
  return (console.log)(((compose)(square, add1))(5));
})(function(f, g) {
  return function(x) {
    return (f)((g)(x));
  };
}, function(x) {
  return (x * x);
}, function(x) {
  return (x + 1);
})

如果你在自己电脑上安装了 node.js,你可以用以下命令运行这段代码:

$ runhaskell Lisp.hs example.lsp | node -p
36
undefined
  • 最终练习 : 编译有多个表达式的程序而非仅编译一个表达式。

via: https://gilmi.me/blog/post/2016/10/14/lisp-to-js

作者:Gil Mizrahi 选题:oska874 译者:BriFuture 校对:wxy

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

几天前,Lucas Holt 宣布发布 MidnightBSD 1.0。让我们快速看一下这个新版本中包含的内容。

什么是 MidnightBSD?

MidnightBSD

MidnightBSD 是 FreeBSD 的一个分支。Lucas 创建了 MightnightBSD,这成为桌面用户和 BSD 新手的一个选择。他想创造一个能让人们快速体验 BSD 桌面的东西。他认为其他发行版过于关注服务器市场。

MidnightBSD 1.0 中有什么?

根据发布说明视频),1.0 中的大部分工作都是更新基础系统,改进包管理器和更新工具。新版本与 FreeBSD 10-Stable 兼容。

Mports(MidnightBSD 的包管理系统)已经升级支持使用一个命令安装多个包。mport upgrade 命令已经修复。Mports 现在会跟踪已弃用和过期的包。它还引入了新的包格式。

其他变化包括:

  • 现在支持 ZFS 作为启动文件系统。以前,ZFS 只能用于附加存储。 * 支持 NVME SSD。 * AMD Ryzen 和 Radeon 的支持得到了改善。 * Intel、Broadcom 和其他驱动程序已更新。 * 已从 FreeBSD 移植 bhyve 支持。 * 传感器框架已被删除,因为它导致锁定问题。 * 删除了 Sudo 并用 OpenBSD 中的 doas 替换。 * 增加了对 Microsoft hyper-v 的支持。

升级之前

如果你当前是 MidnightBSD 的用户或正在考虑尝试新版本,那么还是再等一会。Lucas 目前正在重建软件包以支持新的软件包格式和工具。他还计划在未来几个月内升级软件包和移植桌面环境。他目前正致力于移植 Firefox 52 ESR,因为它是最后一个不需要 Rust 的版本。他还希望将更新版本的 Chromium 移植到 MidnightBSD。我建议关注 MidnightBSD 的 Twitter

0.9 怎么回事?

你可能注意到 MidnightBSD 的先前版本是 0.8.6。你现在可能想知道“为什么跳到 1.0”?根据 Lucas 的说法,他在开发 0.9 时遇到了几个问题。事实上,他重试好几次。他最终采用与 0.9 分支不同的方式,并变成了 1.0。有些软件包在 0.* 系列也有问题。

需要帮助

目前,MidnightBSD 项目几乎是 Lucas Holt 一个人的作品。这是其发展缓慢的主要原因。如果你有兴趣帮忙,可以通过 Twitter 与他联系。

发布公告视频中。Lucas 说他遇到了上游项目接受补丁的问题。他们似乎认为 MidnightBSD 太小了。这通常意味着他必须从头开始移植应用。

想法

我对劣势者有一个想法。在我接触的所有 BSD 中,这个外号最适合 MidnightBSD。一个人想要创建一个轻松的桌面体验。当前只有一个其他的 BSD 在尝试做相似的事情:Project Trident。我想这是 BSD 成功的真正的阻碍。Linux 成功是因为人们可以快速容易地安装它。希望 MidnightBSD 为 BSD 做到这一点,但是还有很长的路要走。

你有没有用过 MidnightBSD?如果没有,你最喜欢的 BSD 是什么?我们应该涵盖哪些其他 BSD 主题?请在下面的评论中告诉我们。

如果你觉得这篇文章有趣,请花一点时间在社交媒体,Hacker News 或 Reddit 上分享它。


via: https://itsfoss.com/midnightbsd-1-0-release/

作者:John Paul 选题:lujun9972 译者:geekpi 校对:wxy

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

你可能已经知道,我们使用 mv 命令在类 Unix 操作系统中重命名或者移动文件和目录。 但是,mv 命令不支持一次重命名多个文件。 不用担心。 在本教程中,我们将学习使用 Linux 中的 mmv 命令一次重命名多个文件。 此命令用于在类 Unix 操作系统中使用标准通配符批量移动、复制、追加和重命名文件。

在 Linux 中一次重命名多个文件

mmv 程序可在基于 Debian 的系统的默认仓库中使用。 要想在 Debian、Ubuntu、Linux Mint 上安装它,请运行以下命令:

$ sudo apt-get install mmv

我们假设你在当前目录中有以下文件。

$ ls
a1.txt a2.txt a3.txt

现在,你想要将所有以字母 “a” 开头的文件重命名为以 “b” 开头的。 当然,你可以在几秒钟内手动执行此操作。 但是想想你是否有数百个文件想要重命名? 这是一个非常耗时的过程。 这时候 mmv 命令就很有帮助了。

要将所有以字母 “a” 开头的文件重命名为以字母 “b” 开头的,只需要运行:

$ mmv a\* b\#1

让我们检查一下文件是否都已经重命名了。

$ ls
b1.txt b2.txt b3.txt

如你所见,所有以字母 “a” 开头的文件(即 a1.txta2.txta3.txt)都重命名为 b1.txtb2.txtb3.txt

解释

在上面的例子中,第一个参数(a\*)是 “from” 模式,第二个参数是 “to” 模式(b\#1)。根据上面的例子,mmv 将查找任何以字母 “a” 开头的文件名,并根据第二个参数重命名匹配的文件,即 “to” 模式。我们可以使用通配符,例如用 *?[] 来匹配一个或多个任意字符。请注意,你必须转义使用通配符,否则它们将被 shell 扩展,mmv 将无法理解。

“to” 模式中的 #1 是通配符索引。它匹配 “from” 模式中的第一个通配符。 “to” 模式中的 #2 将匹配第二个通配符(如果有的话),依此类推。在我们的例子中,我们只有一个通配符(星号),所以我们写了一个 #1。并且,# 符号也应该被转义。此外,你也可以用引号括起模式。

你甚至可以将具有特定扩展名的所有文件重命名为其他扩展名。例如,要将当前目录中的所有 .txt 文件重命名为 .doc 文件格式,只需运行:

$ mmv \*.txt \#1.doc

这是另一个例子。 我们假设你有以下文件。

$ ls
abcd1.txt abcd2.txt abcd3.txt

你希望在当前目录下的所有文件中将第一次出现的 “abc” 替换为 “xyz”。 你会怎么做呢?

很简单。

$ mmv '*abc*' '#1xyz#2'

请注意,在上面的示例中,模式被单引号括起来了。

让我们检查下 “abc” 是否实际上被替换为 “xyz”。

$ ls
xyzd1.txt xyzd2.txt xyzd3.txt

看到没? 文件 abcd1.txtabcd2.txtabcd3.txt 已经重命名为 xyzd1.txtxyzd2.txtxyzd3.txt

mmv 命令的另一个值得注意的功能是你可以使用 -n 选项打印输出而不是重命名文件,如下所示。

$ mmv -n a\* b\#1
a1.txt -> b1.txt
a2.txt -> b2.txt
a3.txt -> b3.txt

这样,你可以在重命名文件之前简单地验证 mmv 命令实际执行的操作。

有关更多详细信息,请参阅 man 页面。

$ man mmv

更新:Thunar 文件管理器

Thunar 文件管理器默认具有内置批量重命名选项。 如果你正在使用 Thunar,那么重命名文件要比使用 mmv 命令容易得多。

Thunar 在大多数 Linux 发行版的默认仓库库中都可用。

要在基于 Arch 的系统上安装它,请运行:

$ sudo pacman -S thunar

在 RHEL、CentOS 上:

$ sudo yum install thunar

在 Fedora 上:

$ sudo dnf install thunar

在 openSUSE 上:

$ sudo zypper install thunar

在 Debian、Ubuntu、Linux Mint 上:

$ sudo apt-get install thunar

安装后,你可以从菜单或应用程序启动器中启动批量重命名程序。 要从终端启动它,请使用以下命令:

$ thunar -B

批量重命名方式如下。

单击“+”,然后选择要重命名的文件列表。 批量重命名可以重命名文件的名称、文件的后缀或者同时重命名文件的名称和后缀。 Thunar 目前支持以下批量重命名:

  • 插入日期或时间
  • 插入或覆盖
  • 编号
  • 删除字符
  • 搜索和替换
  • 大写或小写

当你从选项列表中选择其中一个条件时,你将在“新名称”列中看到更改的预览,如下面的屏幕截图所示。

选择条件后,单击“重命名文件”选项来重命名文件。

你还可以通过选择两个或更多文件从 Thunar 中打开批量重命名器。 选择文件后,按 F2 或右键单击并选择“重命名”。

嗯,这就是本次的所有内容了。希望有所帮助。更多干货即将到来。敬请关注!

祝快乐!


via: https://www.ostechnix.com/how-to-rename-multiple-files-at-once-in-linux/

作者:SK 选题:lujun9972 译者:Flowsnow 校对:wxy

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

链接是可以将文件和目录放在你希望它们放在的位置的另一种方式。

除了 cpmv 这两个我们在本系列的前一部分中详细讨论过的,链接是可以将文件和目录放在你希望它们放在的位置的另一种方式。它的优点是可以让你同时在多个位置显示一个文件或目录。

如前所述,在物理磁盘这个级别上,文件和目录之类的东西并不真正存在。文件系统是为了方便人类使用,将它们虚构出来。但在磁盘级别上,有一个名为 分区表 partition table 的东西,它位于每个分区的开头,然后数据分散在磁盘的其余部分。

虽然有不同类型的分区表,但是在分区开头的那个表包含的数据将映射每个目录和文件的开始和结束位置。分区表的就像一个索引:当从磁盘加载文件时,操作系统会查找表中的条目,分区表会告诉文件在磁盘上的起始位置和结束位置。然后磁盘头移动到起点,读取数据,直到它到达终点,您看:这就是你的文件。

硬链接

硬链接只是分区表中的一个条目,它指向磁盘上的某个区域,表示该区域已经被分配给文件。换句话说,硬链接指向已经被另一个条目索引的数据。让我们看看它是如何工作的。

打开终端,创建一个实验目录并进入:

mkdir test_dir
cd test_dir

使用 touch 创建一个文件:

touch test.txt

为了获得更多的体验(?),在文本编辑器中打开 test.txt 并添加一些单词。

现在通过执行以下命令来建立硬链接:

ln test.txt hardlink_test.txt

运行 ls,你会看到你的目录现在包含两个文件,或者看起来如此。正如你之前读到的那样,你真正看到的是完全相同的文件的两个名称: hardlink_test.txt 包含相同的内容,没有填充磁盘中的任何更多空间(可以尝试使用大文件来测试),并与 test.txt 使用相同的 inode:

$ ls -li *test*
16515846 -rw-r--r-- 2 paul paul 14 oct 12 09:50 hardlink_test.txt
16515846 -rw-r--r-- 2 paul paul 14 oct 12 09:50 test.txt

ls-i 选项显示一个文件的 “inode 数值”。“inode” 是分区表中的信息块,它包含磁盘上文件或目录的位置、上次修改的时间以及其它数据。如果两个文件使用相同的 inode,那么无论它们在目录树中的位置如何,它们在实际上都是相同的文件。

软链接

软链接,也称为 符号链接 symlink ,它与硬链接是不同的:软链接实际上是一个独立的文件,它有自己的 inode 和它自己在磁盘上的小块地方。但它只包含一小段数据,将操作系统指向另一个文件或目录。

你可以使用 ln-s 选项来创建一个软链接:

ln -s test.txt softlink_test.txt

这将在当前目录中创建软链接 softlink_test.txt,它指向 test.txt

再次执行 ls -li,你可以看到两种链接的不同之处:

$ ls -li
total 8
16515846 -rw-r--r-- 2 paul paul 14 oct 12 09:50 hardlink_test.txt
16515855 lrwxrwxrwx 1 paul paul 8 oct 12 09:50 softlink_test.txt -> test.txt
16515846 -rw-r--r-- 2 paul paul 14 oct 12 09:50 test.txt

hardlink_test.txttest.txt 包含一些文本并且字面上占据相同的空间。它们使用相同的 inode 数值。与此同时,softlink_test.txt 占用少得多,并且具有不同的 inode 数值,将其标记为完全不同的文件。使用 ls-l 选项还会显示软链接指向的文件或目录。

为什么要用链接?

它们适用于带有自己环境的应用程序。你的 Linux 发行版通常不会附带你需要应用程序的最新版本。以优秀的 Blender 3D 设计软件为例,Blender 允许你创建 3D 静态图像以及动画电影,人人都想在自己的机器上拥有它。问题是,当前版本的 Blender 至少比任何发行版中的自带的高一个版本。

幸运的是,Blender 提供可以开箱即用的下载。除了程序本身之外,这些软件包还包含了 Blender 需要运行的复杂的库和依赖框架。所有这些数据和块都在它们自己的目录层次中。

每次你想运行 Blender,你都可以 cd 到你下载它的文件夹并运行:

./blender

但这很不方便。如果你可以从文件系统的任何地方,比如桌面命令启动器中运行 blender 命令会更好。

这样做的方法是将 blender 可执行文件链接到 bin/ 目录。在许多系统上,你可以通过将其链接到文件系统中的任何位置来使 blender 命令可用,就像这样。

ln -s /path/to/blender_directory/blender /home/<username>/bin

你需要链接的另一个情况是软件需要过时的库。如果你用 ls -l 列出你的 /usr/lib 目录,你会看到许多软链接文件一闪而过。仔细看看,你会看到软链接通常与它们链接到的原始文件具有相似的名称。你可能会看到 libblah 链接到 libblah.so.2,你甚至可能会注意到 libblah.so.2 相应链接到原始文件 libblah.so.2.1.0

这是因为应用程序通常需要安装比已安装版本更老的库。问题是,即使新版本仍然与旧版本(通常是)兼容,如果程序找不到它正在寻找的版本,程序将会出现问题。为了解决这个问题,发行版通常会创建链接,以便挑剔的应用程序相信它找到了旧版本,实际上它只找到了一个链接并最终使用了更新的库版本。

有些是和你自己从源代码编译的程序相关。你自己编译的程序通常最终安装在 /usr/local 下,程序本身最终在 /usr/local/bin 中,它在 /usr/local/bin 目录中查找它需要的库。但假设你的新程序需要 libblah,但 libblah/usr/lib 中,这就是所有其它程序都会寻找到它的地方。你可以通过执行以下操作将其链接到 /usr/local/lib

ln -s /usr/lib/libblah /usr/local/lib

或者如果你愿意,可以 cd/usr/local/lib

cd /usr/local/lib

然后使用链接:

ln -s ../lib/libblah

还有几十个案例证明软链接是有用的,当你使用 Linux 更熟练时,你肯定会发现它们,但这些是最常见的。下一次,我们将看一些你需要注意的链接怪异。

通过 Linux 基金会和 edX 的免费 “Linux 简介”课程了解有关 Linux 的更多信息。


via: https://www.linux.com/blog/intro-to-linux/2018/10/linux-links-part-1

作者:Paul Brown 选题:lujun9972 译者:MjSeven 校对:wxy

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

Mathpix 是一个漂亮的小工具,它允许你截取复杂数学方程式的截图,并立即将其转换为 LaTeX 可编辑文本。

Mathpix converts math equations images into LaTeX

LaTeX 编辑器在撰写学术和科学文献时非常出色。

当然它还有一个陡峭的学习曲线。如果你不得不要写复杂的数学方程式,这种学习曲线会变得更加陡峭。

Mathpix 是一个在这方面可以帮助你的小工具。

假设你正在阅读带有数学方程式的文档。如果你想在 LaTeX 文档中使用这些方程,你需要使用你的 LaTeX 技能,并且得有充足的时间。

但是 Mathpix 为您解决了这个问题。使用 Mathpix,你可以截取数学方程式的截图,它会立即为你提供 LaTeX 代码。然后,你可以在你最喜欢的 LaTeX 编辑器中使用此代码。

请参阅该视频中的 Mathpix 使用方式。

视频来源:Reddit 用户 kaitlinmcunningham

不是超酷吗?我想编写 LaTeX 文档最困难的部分是那些复杂的方程式。对于像我这样懒人,Mathpix 是天赐之物。

获取 Mathpix

Mathpix 适用于 Linux、macOS、Windows 和 iOS。暂时还没有 Android 应用。

注意:Mathpix 是一个免费使用的工具,但它不是开源的。

在 Linux 上,Mathpix 有一个 Snap 包。这意味着如果你在 Linux 发行版上启用了 Snap 支持,你可以用这个简单命令安装 Mathpix:

sudo snap install mathpix-snipping-tool

使用 Mathpix 很简单。安装后,打开该工具。你会在顶部面板中找到它。你可以使用键盘快捷键 Ctrl+Alt+M 开始使用 Mathpix 截图。

它会立即将方程图片转换为 LaTeX 代码。代码将被复制到剪贴板中,然后你可以将其粘贴到 LaTeX 编辑器中。

Mathpix 的光学字符识别技术正在被WolframAlpha、微软、谷歌等许多公司用于在处理数学符号时提升工具的图像识别能力。

总而言之,它对学生和学者来说是一个很棒的工具。它是免费使用的,我非常希望它是一个开源工具。但我们无法在生活中得到一切,不是么?

在 LaTeX 中处理数学符号时,你是否使用 Mathpix 或其他类似工具?你如何看待 Mathpix?在评论区与我们分享你的观点。


via: https://itsfoss.com/mathpix/

作者:Abhishek Prakash 选题:lujun9972 译者:geekpi 校对:wxy

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

我不确定有多少 Web 开发者能完全避免使用命令行。就我来说,我从 1997 年上大学就开始使用命令行了,那时的 l33t-hacker 让我着迷,同时我也觉得它很难掌握。

过去这些年我的命令行本领在逐步加强,我经常会去搜寻工作中能用的更好的命令行工具。下面就是我现在使用的用于增强原有命令行工具的列表。

怎么忽略我所做的命令行增强

通常情况下我会用别名将新的增强的命令行工具覆盖原来的命令(如 catping)。

如果我需要运行原来的命令的话(有时我确实需要这么做),我会像下面这样来运行未加修改的原始命令。(我用的是 Mac,你的用法可能不一样)

$ \cat # 忽略叫 "cat" 的别名 - 具体解释: https://stackoverflow.com/a/16506263/22617
$ command cat # 忽略函数和别名

bat > cat

cat 用于打印文件的内容,如果你平时用命令行很多的话,例如语法高亮之类的功能会非常有用。我首先发现了 ccat 这个有语法高亮功能的工具,然后我发现了 bat,它的功能有语法高亮、分页、行号和 git 集成。

bat 命令也能让我在(多于一屏的)输出里使用 / 搜索(和用 less 搜索功能一样)。

 title=

我将别名 cat 指到了 bat 命令:

alias cat='bat'

prettyping > ping

ping 非常有用,当我碰到“糟了,是不是 X 挂了?/我的网不通了?”这种情况下我最先想到的工具就是它了。但是 prettyping(“prettyping” 可不是指“pre typing”)在 ping 的基础上加了友好的输出,这可让我感觉命令行友好了很多呢。

 title=

我也将 ping 用别名链接到了 prettyping 命令:

alias ping='prettyping --nolegend'

fzf > ctrl+r

在终端里,使用 ctrl+r 将允许你在命令历史里反向搜索使用过的命令,这是个挺好的小技巧,尽管它有点麻烦。

fzf 这个工具相比于 ctrl+r 有了巨大的进步。它能针对命令行历史进行模糊查询,并且提供了对可能的合格结果进行全面交互式预览。

除了搜索命令历史,fzf 还能预览和打开文件,我在下面的视频里展示了这些功能。

为了这个预览的效果,我创建了一个叫 preview 的别名,它将 fzf 和前文提到的 bat 组合起来完成预览功能,还给上面绑定了一个定制的热键 ctrl+o 来打开 VS Code:

alias preview="fzf --preview 'bat --color \"always\" {}'"
# 支持在 VS Code 里用 ctrl+o 来打开选择的文件
export FZF_DEFAULT_OPTS="--bind='ctrl-o:execute(code {})+abort'"

htop > top

top 是当我想快速诊断为什么机器上的 CPU 跑的那么累或者风扇为什么突然呼呼大做的时候首先会想到的工具。我在生产环境也会使用这个工具。讨厌的是 Mac 上的 top 和 Linux 上的 top 有着极大的不同(恕我直言,应该是差的多)。

不过,htop 是对 Linux 上的 top 和 Mac 上蹩脚的 top 的极大改进。它增加了包括颜色输出,键盘热键绑定以及不同的视图输出,这对理解进程之间的父子关系有极大帮助。

一些很容易上手的热键:

  • P —— 按 CPU 使用率排序
  • M —— 按内存使用排序
  • F4 —— 用字符串过滤进程(例如只看包括 node 的进程)
  • space —— 锚定一个单独进程,这样我能观察它是否有尖峰状态

 title=

在 Mac Sierra 上 htop 有个奇怪的 bug,不过这个 bug 可以通过以 root 运行来绕过(我实在记不清这个 bug 是什么,但是这个别名能搞定它,有点讨厌的是我得每次都输入 root 密码。):

alias top="sudo htop" # 给 top 加上别名并且绕过 Sierra 上的 bug

diff-so-fancy > diff

我非常确定我是几年前从 Paul Irish 那儿学来的这个技巧,尽管我很少直接使用 diff,但我的 git 命令行会一直使用 diffdiff-so-fancy 给了我代码语法颜色和更改字符高亮的功能。

 title=

在我的 ~/.gitconfig 文件里我用了下面的选项来打开 git diffgit showdiff-so-fancy 功能。

[pager]
    diff = diff-so-fancy | less --tabs=1,5 -RFX
    show = diff-so-fancy | less --tabs=1,5 -RFX

fd > find

尽管我使用 Mac,但我绝不是 Spotlight 的粉丝,我觉得它的性能很差,关键字也难记,加上更新它自己的数据库时会拖慢 CPU,简直一无是处。我经常使用 Alfred,但是它的搜索功能也不是很好。

我倾向于在命令行中搜索文件,但是 find 的难用在于很难去记住那些合适的表达式来描述我想要的文件。(而且 Mac 上的 find 命令和非 Mac 的 find 命令还有些许不同,这更加深了我的失望。)

fd 是一个很好的替代品(它的作者和 bat 的作者是同一个人)。它非常快而且对于我经常要搜索的命令非常好记。

几个上手的例子:

$ fd cli # 所有包含 "cli" 的文件名
$ fd -e md # 所有以 .md 作为扩展名的文件
$ fd cli -x wc -w # 搜索 "cli" 并且在每个搜索结果上运行 `wc -w`

 title=

ncdu > du

对我来说,知道当前磁盘空间被什么占用了非常重要。我用过 Mac 上的 DaisyDisk,但是我觉得那个程序产生结果有点慢。

du -sh 命令是我经常会运行的命令(-sh 是指结果以“汇总”和“人类可读”的方式显示),我经常会想要深入挖掘那些占用了大量磁盘空间的目录,看看到底是什么在占用空间。

ncdu 是一个非常棒的替代品。它提供了一个交互式的界面并且允许快速的扫描那些占用了大量磁盘空间的目录和文件,它又快又准。(尽管不管在哪个工具的情况下,扫描我的 home 目录都要很长时间,它有 550G)

一旦当我找到一个目录我想要“处理”一下(如删除,移动或压缩文件),我会使用 cmd + 点击 iTerm2 顶部的目录名字的方法在 Finder 中打开它。

 title=

还有另外一个叫 nnn 的替代选择,它提供了一个更漂亮的界面,它也提供文件尺寸和使用情况,实际上它更像一个全功能的文件管理器。

我的 du 是如下的别名:

alias du="ncdu --color dark -rr -x --exclude .git --exclude node_modules"

选项说明:

  • --color dark 使用颜色方案
  • -rr 只读模式(防止误删和运行新的 shell 程序)
  • --exclude 忽略不想操作的目录
  • 安装指引

tldr > man

几乎所有的命令行工具都有一个相伴的手册,它可以被 man <命令名> 来调出,但是在 man 的输出里找到东西可有点让人困惑,而且在一个包含了所有的技术细节的输出里找东西也挺可怕的。

这就是 TL;DR 项目(LCTT 译注:英文里“文档太长,没空去读”的缩写)创建的初衷。这是一个由社区驱动的文档系统,而且可以用在命令行上。就我现在使用的经验,我还没碰到过一个命令没有它相应的文档,你也可以做贡献

 title=

一个小技巧,我将 tldr 的别名链接到 help(这样输入会快一点……)

alias help='tldr'

ack || ag > grep

grep 毫无疑问是一个强力的命令行工具,但是这些年来它已经被一些工具超越了,其中两个叫 ackag

我个人对 ackag 都尝试过,而且没有非常明显的个人偏好,(也就是说它们都很棒,并且很相似)。我倾向于默认只使用 ack,因为这三个字符就在指尖,很好打。并且 ack 有大量的 ack --bar 参数可以使用!(你一定会体会到这一点。)

ackag 默认都使用正则表达式来搜索,这非常契合我的工作,我能使用类似于 --js--html 这种标识指定文件类型搜索。(尽管 agack 在文件类型过滤器里包括了更多的文件类型。)

两个工具都支持常见的 grep 选项,如 -B-A 用于在搜索的上下文里指代“之前”和“之后”。

 title=

因为 ack 不支持 markdown(而我又恰好写了很多 markdown),我在我的 ~/.ackrc 文件里加了以下定制语句:

--type-set=md=.md,.mkd,.markdown
--pager=less -FRX

jq > grep 及其它

我是 jq 的忠实粉丝之一。当然一开始我也在它的语法里苦苦挣扎,好在我对查询语言还算有些使用心得,现在我对 jq 可以说是每天都要用。(不过从前我要么使用 grep 或者使用一个叫 json 的工具,相比而言后者的功能就非常基础了。)

我甚至开始撰写一个 jq 的教程系列(有 2500 字并且还在增加),我还发布了一个网页工具和一个 Mac 上的应用(这个还没有发布。)

jq 允许我传入一个 JSON 并且能非常简单的将其转变为一个使用 JSON 格式的结果,这正是我想要的。下面这个例子允许我用一个命令更新我的所有 node 依赖。(为了阅读方便,我将其分成为多行。)

$ npm i $(echo $(\
    npm outdated --json | \
    jq -r 'to_entries | .[] | "\(.key)@\(.value.latest)"' \
))

上面的命令将使用 npm 的 JSON 输出格式来列出所有过期的 node 依赖,然后将下面的源 JSON 转换为:

{
    "node-jq": {
        "current": "0.7.0",
        "wanted": "0.7.0",
        "latest": "1.2.0",
        "location": "node_modules/node-jq"
    },
        "uuid": {
        "current": "3.1.0",
        "wanted": "3.2.1",
        "latest": "3.2.1",
        "location": "node_modules/uuid"
    }
}

转换结果为:

[email protected]
[email protected]

上面的结果会被作为 npm install 的输入,你瞧,我的升级就这样全部搞定了。(当然,这里有点小题大做了。)

很荣幸提及一些其它的工具

我也在开始尝试一些别的工具,但我还没有完全掌握它们。(除了 ponysay,当我打开一个新的终端会话时,它就会出现。)

你有什么好点子吗?

上面是我的命令行清单。你的呢?你有没有试着去增强一些你每天都会用到的命令呢?请告诉我,我非常乐意知道。


via: https://remysharp.com/2018/08/23/cli-improved

作者:Remy Sharp 选题:lujun9972 译者:DavidChenLiang 校对:pityonline, wxy

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