其實 這篇文章 在去年暑假就已經寫好,不過最近我的 pull request 才被合併,已經快忘掉了。現修繕後發出來。
規則
三鍵成碼一項程式設計比賽,要求參賽者僅用三個字母或數字以及任意數量的符號編碼輸出 Hello, World!,並且檔案體積最小者獲勝。以下是詳細規則:
- 參賽選手自行挑選三個「字母 / 數字」來編寫程式碼,程式碼中的「字母 / 數字」只能且必須 有這三個(大小寫算兩個不同的鍵)
- 除此三個「字母 / 數字」外,額外允許使用普通鍵盤上的所有 ASCII 符號(例如
{,;等)- 該程式執行後輸出
Hello, World!, 有且僅有該內容,要求分毫不差,注意大小寫和符號- 上傳的所有檔案體積最小者獲勝!(結果截圖、說明文件等的不算)
- 不限程式語言,但禁止使用極小眾語言;我們對極小眾的定義是:Github 語言統計 上從未出現過的語言
- 所有的依賴條件均需是在本比賽開始前就是已有的;否則請上傳,作為檔案內容統計的一部分!
- 要求使用平庸的編譯/執行命令、測試環境、檔名,除平庸部分外,其餘部分也需要符合上述 1, 2 兩條的要求
- 靜態型別語言的入口函式名,不受 1 中的限制。例如 C 語言的
int main, Java 的public static void main等
截至 2023 年 3 月,PowerShell 在 GitHub 使用量排第 32 名,佔比 0.102%,符合要求。1
程式碼
選擇的 3 個字母:i、e、x。
$i=$?+$?
$e=$i+$i
$xi=$x=$e+$i
$xe=""+$x+$x
$xx=""+$x+--$e
$x=(""+$?)[$i]
iex "`$ex=""``$x{$xe}"";`$xe=""``$x{$xx}"""
$ii=$e+++$i
$ix=$ii+$i
$ei=$e*$i
$x="``$x{"
iex "`$e=""$x$e$ei}e$x$xi$xe}$x$xi$xe}$x$xi$ex}, $x$ii$ix}$x$xi$ex}$x$ix$i}$x$xi$xe}$x$xi$e}!"""
$e壓成一行時語句結尾需要有分號。
$i=$?+$?;$e=$i+$i;$xi=$x=$e+$i;$xe=""+$x+$x;$xx=""+$x+--$e;$x=(""+$?)[$i];iex "`$ex=""``$x{$xe}"";`$xe=""``$x{$xx}""";$ii=$e+++$i;$ix=$ii+$i;$ei=$e*$i;$x="``$x{";iex "`$e=""$x$e$ei}e$x$xi$xe}$x$xi$xe}$x$xi$ex}, $x$ii$ix}$x$xi$ex}$x$ix$i}$x$xi$xe}$x$xi$e}!""";$e環境
PowerShell 7.2.5,需要自行安裝,Windows 自帶的版本較舊,沒有 Unicode 跳脫字元。
程式碼可以在終端直接貼上,或者透過 .\x.ps1 執行。
原理
選擇字母
讓我們先來看看其他提交者所用的語言中是怎麼做到的:
- JavaScript 有 JSFuck 這種東西,甚至可以只有符號,自然是不消說的
- Python 版本中選擇了
exec——Python 中的eval只能執行單純的表示式,無法建立變數,而且需要 4 個字母;而exec可以執行程式碼塊 - Bash 用了
tr - Ruby 選了
c和另外兩個數字——Ruby 中<<是字串連線運算子,$>是stdout,後面用格式化字串"%c"和一個整數陣列 - C 利用了 UB 和不平凡的編譯命令
- PHP 選了
H、e、l,剩下的字元驚為天人,不知道是利用了什麼隱式型別轉換,看不懂
PowerShell 中無法對 Char 進行加減操作,顯式地進行型別轉換也需要類似 [char]65 至少 4 個字母。2 所以只能考慮 eval。PowerShell 中的命令名都很長,不過 Invoke-Expression 有別名 iex。另外有一個可以執行字串的運算子 &,但是隻能是命令名,不能帶引數。3
第一個數字
因為不能直接出現數字了,所以需要想辦法得到第一個 Int 型別的值。發現陣列下標為空字串 '' 時可以得到陣列的第一個元素,但是如果想要後面的字母的話,PowerShell 並沒有提供 pop 等函式。4
數字字面量也都至少需要有 0 出現。所以只能透過其他型別隱式轉換出 Int 來。
$i = $? + $?$?表示上一條命令的返回值,在初始時和上一條命令沒有出錯時為True。5- PowerShell 中的
==是-eq,需要額外的字母。 - 當 Bool 型別轉換為整數時,
True是1,故$i為2。6
剩下的大部分操作都是在構造其他數字,程式碼的長度應該可以再壓縮一點。
構造 Unicode
$x = ("" + $?)[$i]+運算子的兩個引數型別不一致的時候,會將第二個引數隱式轉換為第一個引數的型別。故此處$e為 String"True"。
至此,我們有了所需的數字和字母 u,可以用 "u{x}" 來生成一切字元了。不過,"Hello World!" 除了可以直接輸入的 e、空格和 !,十六進位制值中還需要 c 和 f。幸好這兩個的編碼分別為 63 和 66,沒有 a-f 出現。$e 實際上就是:
$e = "``u{48}e``u{6c}``u{6c}``u{6f}, ``u{57}``u{6f}``u{72}``u{6c}``u{64}!";跳脫字元
iex "`$ex=""``$x{$xe}"";`$xe=""``$x{$xx}"""- 字串用雙引號的好處是可以直接嵌入變數。如果要 escape 的話,需要在
$、"、` 前面加上 `;"也可以自身重複兩次""來 escape。7
列印值
PowerShell 預設列印出前一個表示式的值,所以不需要拼湊出 "echo" 再 iex 什麼的。
其他可能的方法
- 變數賦值時可以直接是命令的輸出。比如舊版 PowerShell 可以透過
$ls = ls; $ls[x][x]拿到 ls 命令表頭具體的一個 Char,或許有的命令的輸出包含全部所需的字母。但是新版中大部分命令的輸出不再是字串而是物件了,兩次下標拿到的仍然和原結果一樣。 $error是一個存放了錯誤資訊字串的陣列。8 不過,錯誤會直接輸出,不符合「該程式執行後輸出 Hello, World!, 有且僅有該內容,要求分毫不差」的要求。
---
版权许可
- 本作品 采用 知识共享 署名—非商业性使用 4.0 国际许可协议(CC BY-NC 4.0 International)许可,阁下可自由地共享(复制、发行) 和演绎(修改、转换或二次创作) 这一作品,唯须遵守许可协议条款。

评论