Nextflow scripting
Nextflow脚本语言是Groovy编程语言的扩展。Groovy是用于Java虚拟机的强大编程语言。Nextflow语法专门用于以声明的方式简化计算管道的编写。
Nextflow可以执行任何Groovy代码段或使用JVM平台的任何库。
详细了解Groovy编程语言,请参考这些链接:
Groovy User Guide
Groovy Cheat sheet
Groovy in Action
下面是关于Nextflow脚本语言中使用的最重要的语言结构的速成课程。
!Warning
Nextflow使用UTF-8作为源文件和应用程序文件的默认文件字符编码。在使用您最喜欢的文本编辑器编辑Nextflow脚本时,请确保使用UTF-8编码。
Language basics
Hello world
打印内容就像使用print或println方法一样简单。
println "Hello, World!"
两者之间的唯一区别是println方法隐式地向打印字符串追加一个 new line character(在输出结果末尾增加一行)。
Variables(变量)
要定义一个变量,只需给它赋一个值:
x = 1
println xx = new java.util.Date()
println xx = -3.1499392
println xx = false
println xx = "Hi"
println x
Lists(列表)
List对象可以通过将列表项放在方括号中来定义:
myList = [1776, -1, 33, 99, 0, 928734928763]
您可以使用方括号符号访问列表中的给定项(索引从0开始):
println myList[0]
为了获得列表的长度,使用size方法:
println myList.size()
了解更多关于列表的信息:
Groovy Lists tutorial
Groovy List SDK
Java List SDK
Maps
Maps用于存储associative arrays or dictionaries。它们是异构的、命名的数据的无序集合:
scores = [ "Brett":100, "Pete":"Did not finish", "Andrew":86.87934 ]
注意,存储在Map中的每个值可以是不同的类型。Brett是整数,Pete是字符串,Andrew是浮点数。
可以通过两种方式访问Map中的值:
println scores["Pete"]
println scores.Pete
要向Map添加数据或修改映射,其语法类似于向列表添加值:
scores["Pete"] = 3
scores["Cedric"] = 120
了解更多关于Map的信息:
Groovy Maps tutorial
Groovy Map SDK
Java Map SDK
多重赋值(Multiple assignment)
数组或列表对象可以用于一次给多个变量赋值:
(a, b, c) = [10, 20, 'foo']
assert a == 10 && b == 20 && c == 'foo'
赋值操作符左边的三个变量由列表中相应的项传入。
更多关于多重赋值(Multiple assignment )的信息,请参阅Groovy文档。
条件执行(Conditional Execution)
任何编程语言最重要的特性之一就是能够在不同的条件下执行不同的代码。最简单的方法是使用if构造:
x = Math.random()
if( x < 0.5 ) {
println "You lost."
}
else {
println "You won!"
}
字符串(Strings)
字符串可以用单引号或双引号('或")来定义:
println "he said 'cheese' once"
println 'he said "cheese!" again'
字符串可以用+连接:
a = "world"
print "hello " + a + "\n"
字符串嵌入值(String interpolation)
单引号字符串和双引号字符串之间有一个重要的区别:双引号字符串支持变量嵌入值,而单引号字符串不支持。
在实践中,双引号字符串可以包含任意变量的值(在其名称前加上$字符前缀),也可以包含任何表达式的值(使用${expression}语法),类似于Bash/shell脚本:
foxtype = 'quick'
foxcolor = ['b', 'r', 'o', 'w', 'n']
println "The $foxtype ${foxcolor.join()} fox"x = 'Hello'
println '$x + $y'
这段代码打印:
The quick brown fox
$x + $y
多行字符串(Multi-line strings)
跨越多行的文本块可以通过使用三个单引号或双引号来定义:
text = """
hello there James
how are you today?
"""
!Note
和之前一样,双引号内的多行字符串支持变量嵌入值,而单引号内的多行字符串不支持。
在Bash/shell脚本中,用\
字符结束多行字符串中的一行,可以防止用一个新行字符将该行与下一行分隔开:
myLongCmdline = """ blastp \
-in $input_query \
-out $output_file \
-db $blast_database \
-html
"""result = myLongCmdline.execute().text
在前面的例子中,blastp
和它的-in
、-out
、-db
和-html
参数实际上是一行。
内置变量(Implicit variables)
脚本内置变量
以下变量是在脚本全局执行范围内内置的:
Name | Description |
---|---|
baseDir | The directory where the main workflow script is located (deprecated in favour of projectDir since 20.04.0). |
launchDir | The directory where the workflow is run (requires version 20.04.0 or later). |
moduleDir | The directory where a module script is located for DSL2 modules or the same as projectDir for a non-module script (requires version 20.04.0 or later). |
nextflow | Dictionary like object representing nextflow runtime information (see Nextflow metadata). |
params | Dictionary like object holding workflow parameters specifing in the config file or as command line options. |
projectDir | The directory where the main script is located (requires version 20.04.0 or later). |
workDir | The directory where tasks temporary files are created. |
workflow | Dictionary like object representing workflow runtime information (see Runtime metadata). |
配置内置变量(Configuration implicit variables)
在Nextflow配置文件中内置了以下变量::
Name | Description |
---|---|
baseDir | The directory where the main workflow script is located (deprecated in favour of projectDir since 20.04.0). |
launchDir | The directory where the workflow is run (requires version 20.04.0 or later). |
projectDir | The directory where the main script is located (requires version 20.04.0 or later). |
流程内置变量(Process implicit variables)
在流程定义范围内,task内置变量是可用的,它允许访问当前任务配置指令。例如
process foo {
script:
"""
some_tool --cpus $task.cpus --mem $task.memory
"""
}
在上面的代码中,task.cpu报告 cpus directive
和task.memorymemory directive
的当前值取决于工作流配置文件中给出的实际设置。
详情请参阅Process directives。
Closures
简单地说,closure是可以作为参数传递给函数的代码块。因此,您可以定义一段代码,然后像传递字符串或整数那样传递它。准确说,您可以创建定义为first class objects的函数。
square = { it * it }
表达式it * it周围的花括号告诉脚本解释器将该表达式视为代码。it标识符是一个内置变量,表示在调用函数时传递给函数的值。
一旦编译,函数对象将被赋值给变量square,就像前面显示的其他变量赋值一样。eg:
println square(9)
and get the value 81.
这并不是很有趣,直到我们发现可以将函数square作为参数传递给其他函数或方法。一些内置函数接受这样的函数作为参数。列表上的collect方法就是一个例子:
[ 1, 2, 3, 4 ].collect(square)
这个表达式的意思是:创建一个值为1、2、3和4的数组,然后调用它的collect方法,传入上面定义的closure 。collect方法遍历数组中的每个项,调用该项上的closure,然后将结果放入新数组中,结果为:
[ 1, 4, 9, 16 ]
要了解更多可以用closure作为参数调用的方法,请参阅Groovy GDK。
默认情况下,closures 接受一个名为it的参数,但您也可以使用多个自定义命名的参数创建closures。例如,Map.each()方法可以接受一个带两个参数的closure,它将Map中每个键-值对的键和关联值绑定到这个closure上。这里,我们在closure中使用了明显的变量名key和value:
printMapClosure = { key, value ->
println "$key = $value"
}[ "Yue" : "Wu", "Mark" : "Williams", "Sudha" : "Kumari" ].each(printMapClosure)
Prints:
Yue=Wu
Mark=Williams
Sudha=Kumari
closure还有另外两个重要特征。首先,它可以访问定义它的作用域中的变量,以便与它们进行交互。
其次,closure可以以anonymous的方式定义,这意味着closure没有指定名称,而是在需要使用它的地方定义。
作为展示这两个特性的示例,请参见下面的代码:
myMap = ["China": 1 , "India" : 2, "USA" : 3]
result = 0
myMap.keySet().each( { result+= myMap[it] } )
println result
在Groovy documentation中了解关于closure的更多信息。
正则表达式(Regular expressions)
正则表达式是文本处理的Swiss Army knife。它们为程序员提供了从字符串中匹配和提取patterns的能力。
正则表达式可以通过~/pattern/语法和=~和==~操作符使用。
使用=~来检查给定的pattern是否出现在字符串中的任意位置:
assert 'foo' =~ /foo/ // return TRUE
assert 'foobar' =~ /foo/ // return TRUE
使用==~检查字符串是否完全匹配给定的正则表达式pattern。
assert 'foo' ==~ /foo/ // return TRUE
assert 'foobar' ==~ /foo/ // return FALSE
值得注意的是,~操作符从给定的字符串创建Java Pattern对象,而=~操作符创建Java Matcher对象。
x = ~/abc/
println x.class
// prints java.util.regex.Patterny = 'some string' =~ /abc/
println y.class
// prints java.util.regex.Matcher
正则表达式support是从Java导入的。Java的正则表达式语言和API记录在Pattern Java documentation。
您可能还对这篇文章感兴趣:Groovy: Don’t Fear the RegExp。
String replacement
在给定的字符串中替换出现的pattern,请使用replaceFirst和replaceAll方法:
x = "colour".replaceFirst(/ou/, "o")
println x
// prints: colory = "cheesecheese".replaceAll(/cheese/, "nice")
println y
// prints: nicenice
Capturing groups
您可以匹配包含groups的pattern。首先使用=~操作符创建一个匹配器对象。然后,您可以索引 matcher object 来查找matches:matcher[0]返回一个列表,表示字符串中正则表达式的第一个匹配。第一个列表元素是匹配整个正则表达式的字符串,其余元素是匹配每个组的字符串。
下面是它的工作原理:
programVersion = '2.7.3-beta'
m = programVersion =~ /(\d+).(\d+).(\d+)-?(.+)/assert m[0] == ['2.7.3-beta', '2', '7', '3', 'beta']
assert m[0][1] == '2'
assert m[0][2] == '7'
assert m[0][3] == '3'
assert m[0][4] == 'beta'
应用一些syntactic sugar, 您可以在一行代码中完成相同的工作:
programVersion = '2.7.3-beta'
(full, major, minor, patch, flavor) = (programVersion =~ /(\d+).(\d+).(\d+)-?(.+)/)[0]println full // 2.7.3-beta
println major // 2
println minor // 7
println patch // 3
println flavor // beta
删除字符串中的一部分(Removing part of a string)
可以使用正则表达式模式删除部分String值。找到的第一个匹配将被一个空字符串替换:
// define the regexp pattern
wordStartsWithGr = ~/(?i)\s+Gr\w+/// apply and verify the result
('Hello Groovy world!' - wordStartsWithGr) == 'Hello world!'
('Hi Grails users' - wordStartsWithGr) == 'Hi users'
从字符串中删除第一个是5个字符的单词:
assert ('Remove first match of 5 letter word' - ~/\b\w{5}\b/) == 'Remove match of 5 letter word'
从字符串中删除第一个数字及其末尾的空格:
assert ('Line contains 20 characters' - ~/\d+\s+/) == 'Line contains characters'
Files and I/O
要访问和处理文件,请使用file方法,该方法返回给定文件路径字符串的文件系统对象:
myFile = file('some/path/to/my_file.file')
file方法可以引用文件或目录,具体取决于文件系统中的字符串路径所引用的内容。
当使用通配符 *, ?, []和{}时,参数被解释为一个glob path matcher,file方法返回一个列表对象,其中包含文件名与指定模式匹配的文件的路径,如果没有找到匹配,则返回一个空列表:
listOfFiles = file('some/path/*.fa')
!Note
glob模式中的两个星号(**)类似于*,但可以匹配文件系统路径中的任意数量的目录组件。
默认情况下,通配符不匹配目录或隐藏文件。例如,如果你想在结果列表中包含隐藏文件,可以添加可选参数hidden:
listWithHidden = file('some/path/*.fa', hidden: true)
这里是file的可用选项:
Name | Description |
---|---|
glob | When true interprets characters *, ?, [] and {} as glob wildcards, otherwise handles them as normal characters (default: true) |
type | Type of paths returned, either file, dir or any (default: file) |
hidden | When true includes hidden files in the resulting paths (default: false) |
maxDepth | Maximum number of directory levels to visit (default: no limit) |
followLinks | When true follows symbolic links during directory tree traversal, otherwise treats them as files (default: true) |
checkIfExists | When true throws an exception of the specified path do not exist in the file system (default: false) |
!Tip
如果你是一个Java geek,您会有兴趣知道file方法返回一个 Path对象,它允许你使用在Java程序中常见的方法。
See also: Channel.fromPath.
Basic read/write
给定一个file变量(使用前面示例中所示的file方法声明),读取文件就像获取文件的text属性的值一样容易,该属性以字符串值的形式返回文件内容:
print myFile.text
类似地,您可以通过将字符串值分配给文件的text属性来将其保存到文件中:
myFile.text = 'Hello world!'
!Note
现有的文件内容被赋值操作覆盖,它也可以创建不存在的文件。
为了在不删除现有内容的情况下将字符串值追加到文件中,可以使用append方法:
myFile.append('Add this line\n')
或者使用left shift,这是将文本内容追加到文件的更常用的方法:
myFile << 'Add a line more\n'
二进制数据也可以以同样的方式管理,只是使用文件属性bytes而不是text。因此,下面的示例读取文件并以字节数组的形式返回其内容:
binaryContent = myFile.bytes
或者,您可以通过写入操作将字节数组数据缓冲区保存到文件中:
myFile.bytes = binaryBuffer
!Warning
上述方法在单个变量或缓冲区中一次读取和写入所有文件内容。因此,在处理大文件时不建议使用它们,因为大文件需要更有效的内存方法,例如逐行读取文件或使用固定大小的缓冲区。
Read a file line by line
为了逐行读取文本文件,你可以使用file对象提供的readLines()方法,它以字符串列表的形式返回文件内容:
myFile = file('some/my_file.txt')
allLines = myFile.readLines()
for( line : allLines ) {
println line
}
常用写法:
file('some/my_file.txt')
.readLines()
.each { println it }
!Note
readLines()方法会一次读取所有文件内容,并返回包含所有行的列表。因此,不要使用它来读取大文件。
要处理大文件,可以使用方法eachLine,它每次只向内存中读入一行:
count = 0
myFile.eachLine { str ->
println "line ${count++}: $str"
}
高级文件读取操作(Advanced file reading operations)
Reader类和InputStream类分别为读取文本和二进制文件提供了很好的方法。
newReader方法为给定的文件创建一个Reader对象,允许您以单个字符、行或字符数组的形式读取内容:
myReader = myFile.newReader()
String line
while( line = myReader.readLine() ) {
println line
}
myReader.close()
withReader方法的工作原理与此类似,但当您完成文件处理后,它会自动为您调用close方法。因此,前面的例子可以更简单地写成如下:
myFile.withReader {
String line
while( line = it.readLine() ) {
println line
}
}
方法newInputStream和withInputStream的工作方式类似。主要的区别是它们创建了一个用于写入二进制数据的InputStream对象。
下面是最重要的从文件中读取的方法:
Name | Description |
---|---|
getText | Returns the file content as a string value |
getBytes | Returns the file content as byte array |
readLines | Reads the file line by line and returns the content as a list of strings |
eachLine | Iterates over the file line by line, applying the specified closure |
eachByte | Iterates over the file byte by byte, applying the specified closure |
withReader | Opens a file for reading and lets you access it with a Reader object |
withInputStream | Opens a file for reading and lets you access it with an InputStream object |
newReader | Returns a Reader object to read a text file |
newInputStream | Returns an InputStream object to read a binary file |
阅读Reader和InputStream类的Java文档,了解更多关于从文件中读取数据的方法。
高级文件写入操作(Advanced file writing operations)
Writer和OutputStream类分别为文本和二进制文件的编写提供了很好的方式,包括对单个字符或字节的简单操作,以及对大文件的支持。
例如,给定两个文件对象sourceFile和targetFile,下面的代码将第一个文件的内容复制到第二个文件中,用X替换所有U字符:
sourceFile.withReader { source ->
targetFile.withWriter { target ->
String line
while( line=source.readLine() ) {
target << line.replaceAll('U','X')
}
}
}
下面是最重要的写入文件的方法:
Name | Description |
---|---|
setText | Writes a string value to a file |
setBytes | Writes a byte array to a file |
write | Writes a string to a file, replacing any existing content |
append | Appends a string value to a file without replacing existing content |
newWriter | Creates a Writer object that allows you to save text data to a file |
newPrintWriter | Creates a PrintWriter object that allows you to write formatted text to a file |
newOutputStream | Creates an OutputStream object that allows you to write binary data to a file |
withWriter | Applies the specified closure to a Writer object, closing it when finished |
withPrintWriter | Applies the specified closure to a PrintWriter object, closing it when finished |
withOutputStream | Applies the specified closure to an OutputStream object, closing it when finished |
请阅读Writer
、PrintWriter
和OutputStream类的Java文档,了解更多关于将数据写入文件的方法。
列出目录内容(List directory content)
假设您需要遍历您选择的目录。您可以定义指向它的myDir变量:
myDir = file('any/path')
获取目录列表的最简单方法是使用list或listFiles方法,它们返回目录的一级元素(文件和目录)的集合:
allFiles = myDir.list()
for( def file : allFiles ) {
println file
}
!Note
list和listFiles之间的唯一区别是前者返回一个字符串列表,而后者返回一个允许您访问文件元数据的文件对象列表,例如大小、最后修改时间等。
eachFile方法只允许迭代第一级元素(就像listFiles)。与其他each- methods一样,each files接受一个closure作为参数:
myDir.eachFile { item ->
if( item.isFile() ) {
println "{item.size()}"
}
else if( item.isDirectory() ) {
println "${item.getName()} - DIR"
}
}
上述方法有几种变体可用。完整列表见下表。
Name | Description |
---|---|
eachFile | Iterates through first-level elements (files and directories). Read more |
eachDir | Iterates through first-level directories only. Read more |
eachFileMatch | Iterates through files and dirs whose names match the given filter. Read more |
eachDirMatch | Iterates through directories whose names match the given filter. Read more |
eachFileRecurse | Iterates through directory elements depth-first. Read more |
eachDirRecurse | Iterates through directories depth-first (regular files are ignored). Read more |
参见:Channel fromPath方法。
创建目录(Create directories)
给定一个表示不存在目录的文件变量,如下所示:
myDir = file('any/path')
mkdir方法在给定路径上创建一个目录,如果目录创建成功则返回true,否则返回false:
result = myDir.mkdir()
println result ? "OK" : "Cannot create directory: $myDir"
!Note
如果父目录不存在,上述方法将失败并返回false。
mkdirs方法创建以文件对象命名的目录,包括任何不存在的父目录:
myDir.mkdirs()
创建链接(Create links)
对于一个文件,mklink方法为使用指定的路径作为参数的文件创建* file system link *:
myFile = file('/some/path/file.txt')
myFile.mklink('/user/name/link-to-file.txt')
可选参数表:
Name | Description |
---|---|
hard | When true creates a hard link, otherwise creates a soft (aka symbolic) link. (default: false ) |
overwrite | When true overwrites any existing file with the same name, otherwise throws a FileAlreadyExistsException (default: false ) |
复制文件(Copy files)
copyTo
方法将文件复制到新文件或目录中,或将目录复制到新目录中:
myFile.copyTo('new_name.txt')
!Note
如果目标文件已经存在,它将被新的文件替换。还要注意,如果目标是一个目录,源文件将被复制到该目录中,并保持文件的原始名称。
当源文件是一个目录时,它的所有内容都被复制到目标目录:
myDir = file('/some/path')
myDir.copyTo('/some/new/path')
If the target path does not exist, it will be created automatically.
!Tip
copyTo
方法模仿了Linux命令cp -r <source> <target>
,注意:Linux工具通常将以斜杠结尾的路径(e.g. /some/path/name/
)视为目录,而将不以斜杠结尾的路径(e.g. /some/path/name
)视为常规文件,Nextflow(由于使用了Java文件API)将这两个路径视为相同的文件系统对象。如果路径存在,它将根据它的实际类型(即作为一个常规文件或目录)进行处理。如果该路径不存在,则将其视为常规文件,并自动创建任何缺失的父目录。
移动文件(Move files)
您可以使用moveTo
方法移动文件:
myFile = file('/some/path/file.txt')
myFile.moveTo('/another/path/new_file.txt')
!Note
当与目标同名的文件已经存在时,它将被源文件替换。还要注意,当目标是一个目录时,文件将被移动到(或移动到)该目录中,并保持文件的原始名称。
当源目录是一个目录时,所有目录内容都被移动到目标目录:
myDir = file('/any/dir_a')
myDir.moveTo('/any/dir_b')
请注意,上述示例的结果取决于目标目录是否存在。如果目标目录存在,则将源文件移动到目标目录,从而产生路径:
/any/dir_b/dir_a
如果目标目录不存在,则源文件将被重命名为目标名称,从而产生路径:
/any/dir_b
!Tip
moveTo
方法模拟了Linux命令mv <source> <target>,与上面给copyTo
的警告相同。
重命名文件(Rename files)
您可以使用rename to file
方法重命名文件或目录:
myFile = file('my_file.txt')
myFile.renameTo('new_file_name.txt')
删除文件(Delete files)
file方法delete
删除给定路径下的文件或目录,如果操作成功返回true
,否则返回false
:
myFile = file('some/file.txt')
result = myFile.delete()
println result ? "OK" : "Can delete: $myFile"
!Note
此方法只删除不包含任何文件或子目录的目录。要删除一个目录及其所有内容(即删除它可能包含的所有文件和子目录),使用deleteDir
方法。
查看文件属性
以下方法可用于使用file
方法创建的文件变量:
Name | Description |
---|---|
getName | Gets the file name e.g. /some/path/file.txt -> file.txt
|
getBaseName | Gets the file name without its extension e.g. /some/path/file.tar.gz -> file.tar
|
getSimpleName | Gets the file name without any extension e.g. /some/path/file.tar.gz -> file
|
getExtension | Gets the file extension e.g. /some/path/file.txt -> txt
|
getParent | Gets the file parent path e.g. /some/path/file.txt -> /some/path
|
size | Gets the file size in bytes |
exists | Returns true if the file exists, or false otherwise |
isEmpty | Returns true if the file is zero length or does not exist, false otherwise |
isFile | Returns true if it is a regular file e.g. not a directory |
isDirectory | Returns true if the file is a directory |
isHidden | Returns true if the file is hidden |
lastModified | Returns the file last modified timestamp i.e. a long as Linux epoch time |
例如,下面的行打印文件名和大小:
println "File {myFile.size()}"
!Tip
任何以get
前缀开始的方法名称的调用都可以省略get前缀和ending()
圆括号。因此写入myFile. getname()
和myFile.name
是完全相同的,而myFile. getbasename()
和myFile.baseName
是完全相同的,等等。
获取和修改文件权限(Get and modify file permissions)
给定一个表示文件(或目录)的文件变量,getPermissions
方法返回一个9个字符的字符串,使用Linux symbolic notation表示文件的权限,例如rw-rw-r--
:
permissions = myFile.getPermissions()
类似地,setPermissions
方法使用相同的表示法设置文件的权限:
myFile.setPermissions('rwxr-xr-x')
setPermissions
方法的第二个版本用三个数字分别表示所有者、组和其他权限,设置文件的权限:
myFile.setPermissions(7,5,5)
Learn more about File permissions numeric notation.
HTTP/FTP files
Nextflow提供了HTTP/S和FTP协议的透明集成,将远程资源作为本地文件系统对象处理。只需将资源URL指定为文件对象的参数:
pdb = file('http://files.rcsb.org/header/5FID.pdb')
然后,您可以像前面几节所描述的那样将其作为本地文件访问:
println pdb.text
上面的一行代码打印远程PDB文件的内容。前面的部分提供了一些代码示例,展示了如何流式传输或复制文件的内容。
!Note
HTTP/S和FTP文件不支持写入和列表操作。
Counting records
countLines
countLines
方法计算文本文件中的行数。
def sample = file('/data/sample.txt')
println sample.countLines()
以.gz
后缀结尾的文件将被GZIP压缩并自动解压缩。
countFastq
countFastq
方法计算FASTQ格式文件中的records 数。
def sample = file('/data/sample.fastq')
println sample.countFastq()
以.gz
后缀结尾的文件将被GZIP压缩并自动解压缩。
REFERENCE
https://www.nextflow.io/docs/latest/script.html#files-and-i-o