跳转到内容

newLISP 简介/文件操作

来自维基教科书,开放的书籍,开放的世界

文件操作

[编辑 | 编辑源代码]

与文件操作相关的函数可以分为两类:与操作系统交互,以及从文件读写数据。

与文件系统交互

[编辑 | 编辑源代码]

newLISP 中维护着当前工作目录的概念。当您在终端中输入 newLISP 启动 newLISP 时,您的当前工作目录将变为 newLISP 的当前目录。

$ pwd
/Users/me/projects/programming/lisp
$ newlisp
newLISP v.9.3 on OSX UTF-8, execute 'newlisp -h' for more info.

> (env "PWD")
"/Users/me/projects/programming/lisp"
> (exit)

$ pwd
/Users/me/projects/programming/lisp
$


您也可以使用没有参数的 real-path 函数检查您的当前工作目录

(real-path)
;-> "/Users/me/projects/programming/lisp"

但是,当您从其他地方(例如文本编辑器内部)运行 newLISP 脚本时,当前工作目录和其他设置可能会有所不同。因此,如果您不确定,最好使用 change-dir 来建立当前工作目录

(change-dir "/Users/me/Documents")
;-> true

其他环境变量可以使用 env 访问

(env "HOME")
;-> "/Users/me"
(env "USER")
;-> "cormullion"

同样,从文本编辑器而不是在交互式终端会话中运行 newLISP 会影响哪些环境变量可用以及它们的值。

获得正确的目录后,使用 directory 列出其内容

(directory)
;-> ("." ".." ".bash_history" ".bash_profile" ".inputrc" ".lpoptions"
; ".sqlite_history" ".ssh" ".subversion" "bin" "Desktop" "Desktop Folder"
; "Documents" "Library" ...

等等。请注意,它为您提供的是相对文件名,而不是绝对路径名。 directory 也可以列出除当前工作目录之外的目录的内容,只要您提供路径即可。在这种扩展形式中,您可以使用正则表达式来过滤内容

(directory "./")                        ; just a pathname
;-> ("." ".." ".bash_history" ".bash_profile" ".inputrc" ".lpoptions"
; ".sqlite_history" ".ssh" ".subversion" "bin" "Desktop" "Desktop Folder"
; "Documents" "Library" ...

(directory "./" {^[^.]})                ; exclude files starting "."
;-> ("bin" "Desktop" "Desktop Folder" "Documents" "Library" ... )

同样,请注意结果相对于当前工作目录。存储您列出的目录的路径通常很有用,以防您以后需要使用它来构建完整路径名。 real-path 返回文件或目录的完整路径名,无论是在当前工作目录中

(real-path ".subversion")
;-> "/Users/me/.subversion"

还是由另一个相对路径名指定

(real-path "projects/programming/lisp/lex.lsp")
;-> "/Users/me/projects/programming/lisp/lex.lsp"

要查找磁盘上项目包含的目录,您只需从完整路径名中删除文件名即可

(set 'f "lex.lsp")
(replace f (real-path f) "")
;-> "/Users/me/projects/programming/lisp/"

顺便说一下,如果文件名在路径中更早的位置也作为目录名出现,则此方法并不总是有效。一个简单的解决方案是使用 $ 选项对仅出现在路径名末尾的 f 进行正则表达式搜索

(replace (string f "\$") (real-path f) "" 0)

要递归地扫描文件系统的某个部分,请使用一个递归调用自身的函数。这里只打印完整路径名

(define (search-tree dir)
 (dolist (item (directory dir {^[^.]}))
   (if (directory? (append dir item))
    ; search the directory
    (search-tree (append dir item "/"))
    ; or process the file
    (println (append dir item)))))
    
(search-tree {/usr/share/newlisp/})
/usr/share/newlisp/guiserver/allfonts-demo.lsp
/usr/share/newlisp/guiserver/animation-demo.lsp
...
/usr/share/newlisp/util/newlisp.vim
/usr/share/newlisp/util/syntax.cgi

另见 编辑文件夹和层次结构中的文本文件.

您会发现一些测试函数很有用

  • file? 这个文件或目录存在吗?
  • directory? 这个路径名是目录还是文件?

记住相对路径名和绝对路径名之间的区别

(file? "System")
;-> nil
(file? "/System")
;-> true

文件信息

[编辑 | 编辑源代码]

您可以使用 file-info 获取有关文件的信息。此函数询问操作系统有关文件的信息,并将信息以一系列数字的形式返回

  • 0 是大小
  • 1 是模式
  • 2 是设备模式
  • 3 是用户 ID
  • 4 是组 ID
  • 5 是访问时间
  • 6 是修改时间
  • 7 是状态更改时间

例如,要找出文件的大小,请查看 file-info 返回的第一个数字。以下代码列出了目录中的文件,并包括它们的大小。

(set 'dir {/usr/share/newlisp/modules})
(dolist (i (directory dir {^[^.]}))
  (set 'item (string dir "/" i))
  (if (not (directory? item))
      (println (format {%7d %-30s} (nth 0 (file-info item)) item))))
  35935 /usr/share/newlisp/modules/canvas.lsp
   6548 /usr/share/newlisp/modules/cgi.lsp
   5460 /usr/share/newlisp/modules/crypto.lsp
   4577 /usr/share/newlisp/modules/ftp.lsp
  16310 /usr/share/newlisp/modules/gmp.lsp
   4273 /usr/share/newlisp/modules/infix.lsp
  12973 /usr/share/newlisp/modules/mysql.lsp
  16606 /usr/share/newlisp/modules/odbc.lsp
   9865 /usr/share/newlisp/modules/pop3.lsp
  12835 /usr/share/newlisp/modules/postgres.lsp
  31416 /usr/share/newlisp/modules/postscript.lsp
   4337 /usr/share/newlisp/modules/smtp.lsp
  10433 /usr/share/newlisp/modules/smtpx.lsp
  16955 /usr/share/newlisp/modules/sqlite3.lsp
  21807 /usr/share/newlisp/modules/stat.lsp
   7898 /usr/share/newlisp/modules/unix.lsp
   6979 /usr/share/newlisp/modules/xmlrpc-client.lsp
   3366 /usr/share/newlisp/modules/zlib.lsp


请注意,我们将目录的名称存储在 dir 中。 directory 函数返回相对文件名,但您必须将绝对路径名字符串传递给 file-info,除非字符串引用的是当前工作目录中的文件。

您可以使用隐式寻址来选择所需的项目。因此,不是(nth 0 (file-info item)),您可以写(file-info item 0).

MacOS X:资源分支

[编辑 | 编辑源代码]

如果您在 MacOS X 上尝试了之前的脚本,您可能会注意到有些文件的大小为 0 字节。这可能表明存在从经典(即旧)Macintosh 时代继承的双分支系统。使用以下版本访问文件的资源分支。字体文件夹是日益罕见的资源分支的绝佳搜寻地

(set 'dir "/Users/me/Library/Fonts") ; fonts folder
(dolist (i (directory dir "^[^.]"))
 (set 'item (string dir "/" i))
  (and
    (not (directory? item))          ; don't do folders
    (println 
      (format "%9d DF %-30s" (nth 0 (file-info item)) item))
    (file? (format "%s/..namedfork/rsrc" item)) ; there's a resource fork too
    (println (format "%9d RF" 
      (first (file-info (format "%s/..namedfork/rsrc" item)))))))
...
        0 DF /Users/me/Library/Fonts/AvantGarBoo
    26917 RF  
        0 DF /Users/me/Library/Fonts/AvantGarBooObl
    34982 RF  
        0 DF /Users/me/Library/Fonts/AvantGarDem
    27735 RF  
        0 DF /Users/me/Library/Fonts/AvantGarDemObl
    35859 RF  
        0 DF /Users/me/Library/Fonts/ITC Avant Garde Gothic 1
   116262 RF  
...


文件管理

[编辑 | 编辑源代码]

要管理文件,您可以使用以下函数

  • rename-file 重命名文件或目录
  • copy-file 复制文件
  • delete-file 删除文件
  • make-dir 创建新目录
  • remove-dir 删除空目录

例如,要重新编号当前工作目录中的所有文件,以便文件按修改日期排序,您可以编写类似以下内容

(set 'dir {/Users/me/temp/})
(dolist (i (directory dir {^[^.]}))
 (set 'item (string dir "/" i))
 (set 'mod-date (date (file-info item 6) 0 "%Y%m%d-%H%M%S"))
 (rename-file item (string dir "/" mod-date i)))
;-> before
image-001.png
image-002.png
image-003.png
image-004.png

;-> after
20061116-120534image-001.png
20061116-155127image-002.png
20061117-210447image-003.png
20061118-143510image-004.png


(file-info item 6)提取 file-info 返回的结果的修改时间(项目 6)。

在将此类脚本用于实际工作之前,请始终对其进行测试!一个错误的标点符号可能会造成严重破坏。

读写数据

[编辑 | 编辑源代码]

newLISP 有一个很好的输入和输出函数选择。

将文本写入文件的简单方法是 append-file,它将字符串添加到文件的末尾。如果文件不存在,则会创建它。它非常适合创建日志文件和定期写入的文件

(dotimes (x 10) 
 (append-file "/Users/me/Desktop/log.log" 
 (string (date) " logging " x "\n")))

现在我的桌面上有一个文件,其内容如下

Sat Sep 26 09:06:08 2009 logging 0
Sat Sep 26 09:06:08 2009 logging 1
Sat Sep 26 09:06:08 2009 logging 2
Sat Sep 26 09:06:08 2009 logging 3
Sat Sep 26 09:06:08 2009 logging 4
Sat Sep 26 09:06:08 2009 logging 5
Sat Sep 26 09:06:08 2009 logging 6
Sat Sep 26 09:06:08 2009 logging 7
Sat Sep 26 09:06:08 2009 logging 8
Sat Sep 26 09:06:08 2009 logging 9


您不必担心打开和关闭文件。

要将文件的内容一次性加载到符号中,请使用 read-file

(set 'contents (read-file "/usr/share/newlisp/init.lsp.example"))
;-> 
";; init.lsp - newLISP initialization file\n;; gets loaded automatically on 
; ...
(load (append $HOME \"/.init.lsp\")) 'error))\n\n;;;; end of file ;;;;\n\n\n          "

符号 contents 将文件的内容存储为单个字符串。

open 返回一个值,该值充当对文件的引用或“句柄”。您可能希望稍后使用该文件,因此请将引用存储在符号中

(set 'data-file (open "newfile.data" "read"))  ; in current directory

; and later

(close data-file)

使用 read-line 从文件句柄中逐行读取文件。每次使用 read-line 时,下一行将存储在缓冲区中,您可以使用 current-line 函数访问它。读取文件的基本方法如下

(set 'file (open ((main-args) 2) "read")) ; the argument to the script
(while (read-line file) 
   (println (current-line)))               ; just output the line

read-line 会丢弃每行末尾的换行符。 println 会在您提供的文本末尾添加一个换行符。有关参数处理和 main-args 的更多信息,请参见 STDIO.

对于中小型文件,逐行读取源文件比将整个文件一次性加载到内存中要慢得多。例如,本书的源文档大约有 6000 行文本,大约 350KBytes。使用 read-fileparse 处理文件大约快 10 倍,如下所示

(set 'source-text (read-file "/Users/me/introduction.txt"))
(dolist (ln (parse source-text "\n" 0))
     (process-line ln))

而不是使用 read-line,如下所示

(set 'source-file (open "/Users/me/introduction.txt" "read"))
(while (read-line source-file)     
     (process-line (current-line)))

device 函数是将输出在控制台和文件之间切换的便捷方法

(set 'output-file (open "/tmp/file.txt" "write"))
(println "1: this goes to the console")
(device output-file)
(println "2: this goes to the temp file")
(device 0)
(println "3: this goes to the console")
(close output-file)

directing output to more than one device

假设您的脚本接受一个参数,并且您想将输出写入一个具有相同名称但带有 .out 后缀的文件。试试这个

(device (open (string ((main-args) 2) ".out") "write"))

(set 'file-contents (read-file ((main-args) 2)))

现在您可以处理文件的内容并使用 println 语句输出任何信息。

loadsave 函数用于从文件加载 newLISP 源代码,并将源代码保存到文件。

read-linewrite-line 函数可用于向线程以及文件读取和写入行。参见 向线程读取和写入.

标准输入输出

[编辑 | 编辑源代码]

要从 STDIO(标准输入)读取并写入 STDOUT(标准输出),请使用 read-lineprintln。例如,以下是一个简单的过滤器,它将标准输入转换为小写并将其输出到标准输出

#!/usr/bin/newlisp

(while (read-line) 
   (println (lower-case (current-line))))

(exit)

它可以在 shell 中这样运行

$ ./lower-case.lsp 
HI
hi
HI THERE
hi there
...


以下简短的脚本是由用户 Echeam 提交到用户论坛的有用 newLISP 格式化程序

#!/usr/bin/env newlisp
(set 'indent "    ")
(set 'level 0)

(while (read-line)
    (if (< level 0) (println "ERROR! Too many close-parenthesis. " level))
    (letn ((ln (trim (current-line))) (fc (first ln)))
        (if (!= fc ")") (println (dup indent level) ln))  ; (indent & print
        (if (and (!= fc ";") (!= fc "#"))     ; don't count if line starts with ; or #
            (set 'level 
              (+ level (apply - (count (explode "()") (explode (current-line)))))))
        (if (= fc ")") (println (dup indent level) ln)))  ; (dedent if close-parenthesis
    )

(if (!= level 0) (println "ERROR! Parenthesis not balanced. " level))
(exit)

你可以从命令行运行它,方法是

$ format.lsp < inputfile.lsp


命令行参数

[编辑 | 编辑源代码]

要在命令行使用 newLISP 程序,可以使用 main-args 函数访问参数。例如,如果你创建了这个文件

#!/usr/bin/newlisp 
(println (main-args))
(exit)

使其可执行,然后在 shell 中运行它,你将看到在运行时提供给脚本的参数列表

$ test.lsp 1 2 3
("/usr/bin/newlisp" "/Users/me/bin/test.lsp" "1" "2" "3")
$


main-args 返回传递给程序的参数列表。前两个参数(你可能不想处理它们)分别是 newLISP 程序的路径和正在执行的脚本的路径名

(main-args)
;-> ("/usr/bin/newlisp" "/path/script.lsp" "1" "2" "3")

因此,你可能想要从索引 2 开始处理参数

((main-args) 2)
;-> 1

(main-args 2)       ; slightly simpler
;-> 1

它以字符串形式返回。或者,要处理从索引 2 开始的所有参数,请使用切片

(2 (main-args))
;-> ("1" "2" "3")

参数以字符串列表的形式返回。

通常,你希望在脚本中遍历所有主参数:一个方便的短语是

(dolist (a (2 (main-args)))
 (println a))

另一个更具可读性的等效方法是,它遍历其余参数

(dolist (a (rest (rest (main-args))))
 (println a))

以下是一个简短的脚本,它从文本文件中过滤掉不需要的 Unicode 字符,但允许一些特殊的字符通过

(set 'file (open ((main-args) 2) "read")) ; one file

(define (in-range? n low high)
 ; is n between low and high inclusive?
 (and (<= n high) (>= n low)))
 
(while (read-line file) 
 (dostring (c (current-line))
   (if 
      (or
       (in-range? c 32 127)      ; ascii
       (in-range? c 9 10)        ; tab newline
       (in-range? c 12 13)       ; \f \r
       (= c (int "\0xbb"))       ; right double angle
       (= c (int "\0x25ca"))     ; diamond
       (= c (int "\0x2022"))     ; bullet
       (= c (int "\0x201c"))     ; open double quote
       (= c (int "\0x201d"))     ; close double quote
       )
    (print (char c))))           ; nothing to do
   (println) ; because read-line swallows line endings
)

有关参数处理的更多示例,请参阅 简单倒计时器

华夏公益教科书