用kotlin來開發一個cli工具 | 沒用的技能+1

語言: CN / TW / HK

theme: smartblue

腳手架

腳手架是為了保證各施工過程順利進行而搭設的工作平台

而在程序開發過程中,每個工程或者説公司也都需要一個腳手架工具。通過腳手架命令行的形式簡化開發流程,避免發生一些人為的相對低級的問題,所以這個也就是為什麼叫做腳手架的原因吧。

而由於每個公司的代碼規範都不同,一般情況下會主動讓開發同學進行工程方面的cv操作,就是成本高並且容易出錯。這也就是為什麼我們打算寫一些這樣的工具的原因。

在一般情況下,更多的程序猿會選擇用python去寫,因為腳本語言的靈活性,但是對於一個辣雞安卓來説會增加額外的學習成本,所以這就取決於有沒有天賦了,能不能對一門陌生的語言快速上手了。

這次文章會介紹的是用kotlin去構建一個二進制文件,通過這個來完成腳手架cli工具的建設。

開搞

demo 工程地址TheNext

一開始的啟發在於有時候使用一些第三方工具的時候會提供一個jar包,然後只要輸入java -jar xxx.jar就可以使用這個jar包中的Main函數了。

因為是一個jar包,所以裏面的內容肯定也都是用jvm內的幾種語言來進行編寫的,那麼這就讓我們這種老年選手看到了一絲絲的希望。

開發調試

先建立了一個java工程,然後構建了一個main函數,之後開始進行代碼編寫。但是如果每次都需要先打包之後在通過java -jar來執行的話非常不便利開發並且debug。而且模擬入參也灰常的噁心,你也知道的程序猿都是懶人嗎。

所以我們就借用了unittest的能力,對於入參進行mock進行簡單的調試功能了。

【參考地址](http://github.com/Leifzhang/TheNext/blob/main/impact/src/test/kotlin/com/kronos/mebium/test/Sample.kt)

``` class Sample {

@Test
fun help() {
    Next.main(
        arrayOf(
            "--help"
        )
    )
}

@Test
fun testAndroidModule() {
    val file = File("")
    val moduleName = "strike-freedom"
    val groupName = "com.kronos.common"
    Next.main(
        arrayOf(
            "module", "android",
            "-file", file.absolutePath,
            "-name", moduleName,
            "-group", groupName
        )
    )
}

@Test
fun testAndroidApplication() {
    val file = File("../app/")  
    val projectName = "freedom"
    Next.main(
        arrayOf(
            "project", "android",
            "-name", projectName,
            "-file", file.absolutePath
        )
    )
}

} ```

此處我們將Main函數通過unittest來進行模擬,這樣就可以方便我們在開發階段快速調試腳手架的能力了。

每個方法塊都可以認為是一個運行的入口,通過這個來模擬出程序所需要的入參。從而一邊完成了測試代碼的編寫,一邊完成了調試入口。

jcommander

這是一個讓我們可以更像模像樣的寫一個cli的入參解析工具,即使參數順序是錯亂的,我們仍然能解析出我們想要的數據結構,讓我們的工程看起來更正規一點。而且這個庫也被很多開源項目所使用,基本算的上是千錘百煉了,比如美團的walle

jcommander值得你一個star的

``` @Parameters(commandDescription = "args 參數") class CommandEntity {

@Parameter(
    names = ["-file", "-f"],
    required = true,
    converter = FileConverter::class,
    description = "生成目標文件路徑"
)
lateinit var file: File

@Parameter(
    names = ["-name"], required = true,
    description = "文件名"
)
lateinit var name: String

@Parameter(names = ["-group", "-bundle", "-g", "-b"], description = "唯一標識符")
var group: String? = null

}

```

```

override fun handle(args: Array) { val commandEntity = CommandEntity() JCommander.newBuilder().addObject(commandEntity).build().parse(*args) } ```

實例demo如上,我也是參考了官方demo寫的。通過JCommander將args解析成對應的數據實體結構。

Main 函數聲明

我們要在build.gradle內的jar的task中,聲明當前jar的main函數,作為命令行工具的入口。否則打出來的jar包就會報沒有main函數的異常。

jar { exclude("**/module-info.class") /* from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }*/ manifest { attributes 'Main-Class': 'com.kronos.mebium.Next' } }

其中from的含義就是將一個jar包把所有的依賴都打到一起,從而形成一個fatjar,而後續因為使用了gradle提供的application插件,所以這行被我註釋了。

壓縮模板

我們這個腳手架最核心的就是把一部分工程模板壓縮成一個zip資源文件,打包帶入jar產物中。然後呢我這個人又比較懶,希望每次執行打包的時候都進行一次模板的壓縮替換,所以這裏我通過一部分gradle task來進行執行了。

``` abstract class ZipTask extends DefaultTask { @InputDirectory Provider library = project.objects.property(File)

@OutputFile
Provider<File> outputFile = project.objects.property(File)


@TaskAction
def doAction() {
    def outputFile = outputFile.get()
    createFileSafety(outputFile)
    compress(library.get(), outputFile)
}

static File compress(final File srcDir, final File zipFile) {
    ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))
    srcDir.eachFileRecurse({
        zos.putNextEntry(new ZipEntry(it.path - srcDir.path + (it.directory ? "/" : "")))
        if (it.file) {
            zos << it.bytes
        }
        zos.closeEntry()
    })
    zos.close()
    return zipFile
}

private static File createFileSafety(File file) {
    if (file.exists()) {
        file.delete()
    }
    if (!file.getParentFile().exists()) {
        file.getParentFile().mkdirs()
    }
    return file
}

} ```

首先定義出一個task,然後定義好輸入輸出,輸入的是一個文件夾,輸出的則是一個zip的壓縮文件,輸入輸出的地址由外部來聲明。

```

def moduleTask = project.tasks.register("zipAndroidLib", ZipTask.class) { it.library.set(file("../library")) it.outputFile.set(file("./src/main/resources/zip/android/android.zip")) }

def projectTask = project.tasks.register("zipAndroidProject", ZipTask.class) { it.library.set(file("../project")) it.outputFile.set(file("./src/main/resources/zip/android/project.zip")) }

afterEvaluate { project.tasks.findByName("compileJava").dependsOn(moduleTask) project.tasks.findByName("compileJava").dependsOn(projectTask) } ```

然後直接聲明處兩個task,之後把compileJava依賴到這兩個task上去,這樣就可以保證每次compileJava,這兩個task都會被執行到了。編譯緩存我就不説了,大家自行領悟吧。

java resource 讀取方式 javaClass.classLoader.getResourceAsStream(name) 就可以了。

放飛自我

接下來我們就可以在命令行工具內放飛自我,開始很簡單的通過unittest來進行代碼的編寫和調試了。

我們就可以通過自己熟悉的kotlin或者java來編寫一個簡單的cli工具,從而來進一步的做到基於工程定製化的一些方便的腳手架工具了。

生成最終產物

這裏我們使用了 gradle提供的application plugin,這個插件可以將java jar包裝成一個可執行文件的zip的壓縮包。格式如下圖所示:

image.png

而這個的生成指令就是,通過./gradlew impact:assembleDist 任務生成對應的二進制壓縮包。

這樣的好處就是我們可以省略掉java -jar xxxxx.jar的繁瑣操作,通過可執行文件直接達到我們寫一個cli的便利。

結尾

工程內的代碼還是比較簡單的,有興趣的就自己讀一下,只是一個demo而已。

還是那句因為菜,不想去學一門新語言。如果萬一哪怕我的py在強那麼一點點,我也考慮用py來寫了,哈哈哈哈哈。