The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

珍惜生命,學習 Java 請少用 PowerShell 當成你主要的 Shell 環境

我個人在學習一門技術時,通常不太喜歡過渡依賴 IDE 幫我完成工作,因為方便的背後會錯過許多技術細節,所以我常常會不經意的刻意繞一下遠路。就像我今天在練習用 Maven 開發 Multi-Module Project 的時候,就發現我第一個命令就卡關了,這也太不順了吧。今天這篇文章,我就來說說為什麼建議大家還是少用 PowerShell 來當成你的主要工作環境。

錯誤發生的過程

今天我在跟著實作 Multi-Module Project with Maven 這篇文章時,我發現連第一個命令都無法執行,命令是這樣的:

mvn archetype:generate -DgroupId=com.baeldung -DartifactId=parent-project -B

我執行的時候,卻遭遇到以下錯誤訊息:

[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.066 s
[INFO] Finished at: 2022-09-09T21:21:47+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] The goal you specified requires a project to execute but there is no POM in this directory (G:\Projects\cc). Please verify you invoked Maven from the correct directory. -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MissingProjectException

image

老實說,這個錯誤訊息實在太詭異了,根本完全看不懂,我是要建立一個新專案啊,你幹嘛說我沒有 POM 檔案?

我就算加上了 -X 參數,依然是無法理解的錯誤訊息:

[ERROR] The goal you specified requires a project to execute but there is no POM in this directory (G:\Projects\cc). Please verify you invoked Maven from the correct directory. -> [Help 1]
org.apache.maven.lifecycle.MissingProjectException: The goal you specified requires a project to execute but there is no POM in this directory (G:\Projects\cc). Please verify you invoked Maven from the correct directory.
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:85)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:294)
    at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192)
    at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105)
    at org.apache.maven.cli.MavenCli.execute (MavenCli.java:960)
    at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:293)
    at org.apache.maven.cli.MavenCli.main (MavenCli.java:196)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (Native Method)
    at jdk.internal.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:77)
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke (Method.java:568)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347)
[ERROR]
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MissingProjectException

錯誤發生的原因

完全相同的命令,我跑在 Command Prompt (命令提示字元) 下,是完全沒問題的!這就至少證實了一件事,在 PowerShell 環境下執行傳統的 Win32 程式或批次檔,是不太一樣的。至於哪裡不一樣,不特別去研究還真的說不上來,總之就是鬼打牆。

基本上你可以這樣想,在 Windows 執行任意程式都是以 Command Prompt (命令提示字元) 為基礎環境執行,這點再 50 年也不會改變的。這些年微軟推廣的 PowerShell 環境,其實也無法跳過底層架構,他有自己的命令、語法、情境,但是當你要執行的不是 Cmdlet 的時候,還是會幫你轉譯成命令提示字元下的命令執行,只是這個轉譯的過程沒有很清晰。在我的部落格中,不知道有幾篇文章跟這件事情有關了,總之遇到了就會採一堆雷,浪費一堆時間抓鬼。😠

這裡我整理了幾點差異給大家看看:

  • Command Prompt

    echo -DarchetypeGroupId=org.apache.maven.archetypes
    
  • Windows PowerShell

    由於 echoWrite-Output cmdlet 的 Alias 而已,理論上這個 echo 並非執行 Windows 執行檔或批次檔:

    echo -DarchetypeGroupId=org.apache.maven.archetypes
    

    這裡我用 Git for Windows 內建的 echo.exe 來示範執行:

    . "C:\Program Files\Git\usr\bin\echo.exe" -DarchetypeGroupId=org.apache.maven.archetypes
    

    此時你可以得到真正的轉譯結果,PowerShell 會把參數列中的第一個 . (dot) 分段,一個參數就變成兩個參數了,你說雷不雷!

    -DarchetypeGroupId=org .apache.maven.archetypes
    

我是刻意用 echo.exe 執行給你看,你看得出問題,如果你今天執行的是 mvnjavajavac 的話,你根本看不出參數寫錯,就像我今天遇到的問題一樣,從錯誤訊息完全看不出問題在哪裡,花了好多時間在找錯誤!😒

為什麼 PowerShell 會有這種問題?

我原本以為這是一種語言特性,畢竟 PowerShell 已經推出到這個市場上十多年的時間了,怎麼可能還有這種低級錯誤?

我後來在 PowerShell 的 GitHub Repo 找到 Parameter parsing/passing: an unquoted argument that starts with a "-" (hyphen) is broken in two at the first "." (period) #6291 這個 Issue (議題),發現這還真的是一個 Bug 耶,而且存在很久了,微軟遲遲不修掉,也不說明原因,也不說何時要修。重點是,超多人遇到過跟這個 Bug 相關的錯誤,浪費大家無數的時間,我覺得真的是罪孽阿!😅

那還建議使用 PowerShell 嗎?

老實說,以前我都拒絕使用 PowerShell 環境,因為 Command Prompt 的啟動速度快,打命令也很少遇到問題。但我這幾年開始改用 PowerShell 為我主要的命令列環境,因為功能相對完整許多,像是 PSReadline 就真的好用,還有 Oh My Posh 可以打造超級華麗的 CLI 環境,這些都是我愛用 PowerShell 的原因。

我就以 Apache Maven 的 CLI 為例,我們很常看到有類似 -DgroupId=com.duotify 這樣的參數,或是用 -Pname 指定 Profile 名稱等等,這是在 Java 界常見的參數用法,其實在 Windows 比較少見這種參數用法,所以我之前遇到錯誤的機率沒有很高,但開始寫 Java 就會遇到了,而且會很常遇到!🔥

因為這種寫法,只要遇到參數沒有小數點(.)的時候並不會發生問題,但只要有小數點的時候,你就準備花時間抓鬼了!

解法不是沒有,但因為不是只有你會遇到問題,任何不熟悉 PowerShell 的人都有機會遇到,所以我是這樣建議的:

  1. 盡量以 Command Prompt 做為你主要的 Shell 執行環境

    因為在學習 Java 的時候,我們會看很多網路上的文章,而大多數文章都不會跟你講他是在什麼 Shell 環境下執行。但我的經驗是,只要你用 Bash 或 Command Prompt 通常不太會遇到問題,但並非 100% 喔,因為 Windows 就是雷比較多!😅

    我認為對初學者來說,使用 Command Prompt 是首選,建議大家都這樣用,但非必要,因為 Command Prompt 有他自己的問題,複雜的命令也是非常難懂的,若要要做自動化還是寫 PowerShell 比較好。

  2. 繼續使用 PowerShell 做為你主要的 Shell 執行環境,但要注意參數解析的問題

    我們這段命令:

    mvn archetype:generate -DgroupId=com.baeldung -DartifactId=parent-project -B
    

    在 PowerShell 環境下,你要自己知道遇到參數中有 . 的時候,要改成這樣:

    mvn archetype:generate '-DgroupId=com.baeldung' -DartifactId=parent-project -B
    

    或者就不管這麼多了,所有參數都加上單引號也可以:

    mvn 'archetype:generate' '-DgroupId=com.baeldung' '-DartifactId=parent-project' '-B'
    

    另外還有一種方法,就是透過 cmd /c 'COMMAND' 來執行命令,這可以讓你想傳到 cmd.exe 的命令與參數,全部都當成「字串」來處理,因此就不會有額外的解析行為發生:

    cmd /c 'mvn archetype:generate -DgroupId=com.baeldung -DartifactId=parent-project -B'
    

    這三種都是很棒的解法,尤其是最後一種,大家可以思考一下要不要繼續用 PowerShell 喔!

停用 PowerShell 的命令解析行為

從 PowerShell 3.0 開始,有個「停用解析」功能,你只要在命令後加上 --% 這個特殊的 Token 就可以停用解析,後面的參數就會原封不動的傳入 cmd.exe 執行。

所以我們這段命令:

mvn archetype:generate -DgroupId=com.baeldung -DartifactId=parent-project -B

就可以改成這樣,這個命令就可以在 PowerShell 正常執行:

mvn --% archetype:generate -DgroupId=com.baeldung -DartifactId=parent-project -B

注意: 上述的 mvn 命令其實是個 mvn.cmd 批次檔,並不是個 Cmdlet 喔!

感覺上,這應該是相當理想的一種解決方案,但你可能要注意的地方是,當「停用解析」之後,在 --% 之後的所有參數,都會傳給 mvn.cmd 這個批次檔,所以 --% 之後的所有內容,都是傳入到 cmd.exe 執行的喔!🔥

我寫個簡單的例子給你比較一下就知道了:

  • 底下這段命令會輸出 %JAVA_HOME% 字串

    . "C:\Program Files\Git\usr\bin\echo.exe" %JAVA_HOME%
    

    image

  • 底下這段命令則會輸出 cmd.exe 環境下的 %JAVA_HOME% 環境變數

    . "C:\Program Files\Git\usr\bin\echo.exe" --% %JAVA_HOME%
    

    image

    這種用法將無法讓你在 --% 後面加入任何 PowerShell 變數,這是最大的限制,必須注意!🔥

總結

在 Windows 使用 PowerShell 執行 Java 常見的 CLI 工具,總結就是一個字:「雷」!

老實說,知道了這些地雷般的技術細節之後,我還是會繼續用 PowerShell 啦,因為我的 CLI 用量比較大,只要知道怎樣不踩雷,還是可以安心使用的!😊

相關連結