awk 命令

awk 簡述

awk 命令格式
awk [選項] '程式' file ...
awk 命令列選項

-F, --field-separator

設定欄位分隔字元,預設為「空白字元」或製表字元 (Tab 鍵)。

-v var=value, --asign var=value

將變數 var 設定成 value

記錄和欄位

AWK 可以處理文字檔案和串流 (管道),輸入資料分為「記錄」和「欄位」。awk 一次對一條「記錄」進行操作,直到結尾 (EOF)。
「記錄」由「記錄分隔字元 RS (Record Separator)」來分隔,預設的 RS 是換行字元,表示文字資料中的每一行都是一條記錄,可以使用變數設定不同的 RS
一行記錄可由「欄位分隔字元 FS (Field Separator)」分隔出不同欄位,預設的 FS 是空格或製表字元 (Tab 鍵),可採用命令列選項 -F 或變數設定不同的 FS。每個欄位可由欄位變數 ($) 取得 $1 $2 $3 依此類推,$0 為全部內容,最後一個欄位也可以使用變數 $NF

FS (欄位分隔字元) 設定為 :,將 /etc/passwd 檔案標示出欄位如下:
$grep ubuntu /etc/passwd
ubuntu:x:1000:1000::/home/ubuntu:/bin/bash
$1    :$2    :$4   :$6          :$7($NF)
        :$3       :$5
$0----------------------------------------

awk 程式 (program)

「程式 (program)」是由一系列模式動作對組寫成:
'pattern {action}'

其中 pattern (模式) 表示在資料中找出符合的「記錄(行)」,可為邏輯判斷式或正規表示式 (Regex),當省略 pattern 時,表示匹配所有記錄(行)。
{action} 是在匹配記錄時所執行的動作,由大括號 {} 中的語句 (statement) 組成,{action} 也可省略,省略時預設動作是列印該行的全部內容 (print $0)。
語句 (statement) 可以解讀為「指令」。

一個動作 (action) 有多個指令 (語句) 時,要寫在下一行或以分號 (;) 分開,如下:
awk -F: '
{
  print $1
  print $6
}' /etc/passwd
awk -F: '{print $1; print $6}' /etc/passwd
另外在「控制語句」 (如 if) 中,如果有多個指令也需要由大括號 {} 組成複合語句,如下:
awk -F: '
{
  if ($3 >= 1000) {
    print $1
    print $3
  }
}' /etc/passwd

awk 模式 (pattern)

模式 (pattern) 判斷式

邏輯判斷語法

x == y

相等

x != y

不相等

x > y

大於

x >= y

大於或等於

x < y

小於

x <= y

小於或等於

正規表示式語法

/Regex/

匹配 Regex 表示式,表示式必須以定界字元 / 包圍。

!/Regex/

否定匹配 Regex 表示式。

x ~ y

x 匹配 y 正規表示式。

x !~ y

x 不匹配 y 正規表示式。

AND OR 語法,適用邏輯判斷式及正規表示式。

&&

AND 條件

||

OR 條件

以邏輯判斷式,列出 UID 大於或等於 1000 的帳戶
awk -F: '($3 >= 1000) {print $1, $6}' /etc/passwd

列出用戶名及家目錄為第 1 個欄位及第 6 個欄位 {print $1, $6},欄位變數之間的逗號 (,) 是列印欄位分隔字元 OFS(Output field separator) 預設為空白字元。如果是 {print $1 $6} 並不會把欄位分隔,可採用 " " 字串,在字串中加入空格,如 {print $1 " " $6}

以控制語句,列出 UID 大於或等於 1000 的帳戶
awk -F: '{if ($3 >= 1000) print $1 " " $6}' /etc/passwd
以正規表示式 (Regex) 匹配模式 (pattern),過濾具有家目錄的帳戶
awk -F: '/\/home/ {print $1, $6}' /etc/passwd

將記錄中整行的內容匹配模式 /\/home/,正規表示式必須以定界字元 / 包圍,而字元 / 必須以跳脫字元 \ 轉譯成 \/,內容匹配 /home 才會列印。

匹配 (~) 兩個運算,$6 ~ /\/home/ 為欄位 6 匹配 /home,本例的結果跟上面是一樣,只是上面是不區分欄位。
awk -F: '$6 ~ /\/home/ {print $1, $6}' /etc/passwd
以模式指定記錄範圍
pattern1, pattern2

採用逗號 (,) 區分兩個位置;位置範圍由第一個位置 pattern1 至 (,) 第二個位置 pattern2, 第二個開始匹配是在第一個位置或之後,記錄範圍始終跨越至少一個記錄 (行)。

列印記錄內容 2 至 3 的記錄
seq 5 | awk '$0==2, $0==3'
seq 5 | awk '/2/, /3/'
可混合「邏輯判斷式」及「正規表示式」
seq 5 | awk '$0==2, /3/'
列印記錄內容 3 至 2,由於第二個位置不合乎條件,將列印第一個位置記錄 (3) 至最後記錄 (5)
seq 5 | awk '/3/, /2/'
列印記錄內容 2 或 3 或 Yes 的記錄
echo Yes | awk '/2/ || /3/ || /Yes/'
在範圍模式中,逗號 (,) 是所有運算子中最低的優先權 (即;最後判斷)。因此,範圍模式與另一個運算結合起來:
echo Yes | awk '/2/,/3/ || /Yes/'
程式的意圖是 (/1/,/2/) || /Yes/,但 awk 將其解讀為 /1/, (/2/ || /Yes/),這無法改變或解決;範圍模式不能與其他模式結合:
$echo Yes | awk '(/1/,/2/) || /Yes/'

awk: cmd. line:1: (/1/,/2/) || /Yes/
awk: cmd. line:1:           ^ syntax error
採用「控制語句」來處理:
echo Yes | awk '{if ( ($0>=2 && $0<=3) || ($0=="Yes") ) print}'
多條模式 (pattern)

在程式中可以有多條模式,合乎模式的以不同動作來處理。

awk -F: '
$1=="root" {
  print "The superuser is on line ", NR
}

$1=="www-data" {
  print "The user for the web server is on line ", NR
}
' /etc/passwd
BEGIN 和 END 特殊模式 (pattern)
BEGIN { 尚未讀取前先執行的動作 }

BEGINFILE { 檔案尚未讀取的動作  }

{ 讀取時執行的動作 }

ENDFILE { 檔案讀取後執行的動作 }

END { 讀取完成後執行的動作 }

當程式 (program) 只有 BEGIN 時,並不需要讀取檔案或管道輸入。
在執行 BEGIN 時並未開啟及讀取檔案,BEGINFILE 為開啟檔案,尚未讀取,再執行 讀取時執行的動作,檔案讀取完成後執行 ENDFILE,所有檔案處理完畢後最後執行 END

當傳入多個檔案時,處理檔案按順序 (先開啟左邊的檔案) 把多個檔案當成接續的 (一個) 檔案,其行號 NR 為接續。

awk '
BEGIN {
  print "BEGIN FILENAME is", FILENAME
}
BEGINFILE {
  print "BEGINFILE", FILENAME, NR
}
{
  if (LastFileName != FILENAME) {
    LastFileName = FILENAME
    print "Read file", FILENAME, NR
  }
}
ENDFILE {
  print "ENDFILE", FILENAME, NR
}
END {
  print "END FILENAME is", FILENAME
}
' /etc/passwd /etc/group
BEGIN FILENAME is (1)
BEGINFILE /etc/passwd 0 (2)
Read file /etc/passwd 1 (3)
ENDFILE /etc/passwd 27 (4)
BEGINFILE /etc/group 27
Read file /etc/group 28
ENDFILE /etc/group 81
END FILENAME is /etc/group (5)
1 BEGIN 未開啟及讀取檔案
2 BEGINFILE 開啟檔案,尚未讀取檔案。
3 執行「讀取時的動作」讀取檔案。
4 ENDFILE 檔案讀取完成。
5 END 所有檔案處理完畢。

awk 變數

內建變數

ARGC

命令列輸入參數數量

ARGV

命令列輸入參數內容 (陣列)

NR

(目前) 記錄位置 (行數)。

NF

(目前) 記錄中欄位數。

FILENAME

檔案名稱。

FS

(輸入的) 欄位分隔字元 (Field Separator),預設為「空白字元」或製表字元 (Tab 鍵)。

RS

(輸入的) 記錄分隔字元 (Record Separator),預設為換行字元。

OFS

輸出欄位分隔字元 (Output Field Separator),預設值為空白字元。

ORS

輸出記錄分隔字元 (Output Record Separator),預設值為換行字元。

OFMT

輸出數字格式 (Format for numeric output),預設值為 %.6g

IGNORECASE

匹配或排序時不區分大小寫,預設值為 1。

RSTART

字串函數 match 專用,回傳匹配的 (開始) 位置。

RLENGTH

字串函數 match 專用,回傳匹配的 (內容) 長度。

SUBSEP

(多主鍵) 陣列分隔字元。

FPAT

匹配 FPAT 正規表示法,以匹配內容建立欄位,預設為 [^[:space:]]+ (不為空格的多個字元)。
在程式中同時設定變數 FSFPAT,是以最後設定哪個變數來解析欄位,未設定變數預設是以 FS 來分隔。

CSV 是以逗號 , 來區分欄位,但字串中也會有逗號的情況如 1,"2,b",3,這時只能用 FPAT = "[^,]+|\"[^\"]+\"" 來處理。

上述的正規表示法的意思是
  • 匹配不為逗號 [^,] 多個 (+) 字元

  • |

  • 前後為雙引號 \" 中間不為雙引號 [^\"] 多個 (+) 字元

列印具有家目錄的帳戶及行數
$awk -F: '/\/home/ {print $1,NR}' /etc/passwd
syslog 21
ubuntu 24
BEGIN 在讀取前先設定 FS (輸入) 欄位分隔字元為 :OFS 輸出欄位分隔字元為 ,
awk 'BEGIN {FS=":"; OFS=","} /\/home/ {print $1,NR}' /etc/passwd
以命令列選項 -v 設定內建變數
awk -vFS=: -vOFS=, '/\/home/ {print $1,NR}' /etc/passwd
設定 OFMT 輸出數字格式示例
awk 'BEGIN{OFMT="%.0f"; print 2.5, 3.5, 4.5, 5.5}'
2 4 4 6

awk 的浮點進位為銀行進位法 (Banker’s Rounding) 可簡記為「四捨六入五成雙」,五成雙,尾數「5」的前一位,若雙數則為雙數,若為單數則進位。
如格式化字串為 %.0f,參數輸入 2.5, 3.5, 4.5, 5.5 則輸出 2, 4, 4, 6

自定變數

awk 自定變數可為任意型別,如數字、浮點、字串或陣列。變數名稱不可跟函數名稱相同,如 index = 1 (index 剛好為函數),由於變數區分大小寫,可以改名為 Index = 1

變數內容指定為 16 進制數值,則內容值前置 0x,如 myVar = 0x10 (10 進制為 16),x 或數值 af 不區分為大小寫可混用。
8 進制則為前置 0myVar = 010 (10 進制為 8)。

引用 shell 變數
num=123
awk -v n=$num 'BEGIN {print n}'

awk 控制語句

{action} 控制語句語法
if (conditional)
  then-body-statement
else if (conditional)
  else-if-then-body-statement
else
  else-body-statement

initialization-statement
while (conditional) {
  body-statement
  increment-statement
  break
  continue
}

for (initialization; condition; increment)
  body-statement
  break
  continue

for (variable in array)
  body-statement
  break
  continue

switch (expression) {
case value or regular_expression:
  case-body-statement
  break
default:
  default-body-statement
  break
}

next

exit [return_code]

nextfile
next

處理下一筆記錄,若不為 EOF 重啟循環,回到「讀取時執行的動作」程式開始位置 (不會執行 next 後面的程式)。

awk 函數

print

print 內的逗號 , 是列印 OFS 欄位分隔字元,數字輸出格式為 OFMT (預設為 %.6g),執行 print 後會加印 ORS 輸出記錄分隔字元 (預設為換行字元)。可設定內建變數 OFSOFMTORS 改變預設行為。print $1 $2 並不會把欄位分隔,可採用 " " 字串,在字串中加入空格,如 print $1 " " $2

print 無參數時為 print $0,另外在 awk 程式 (program) 的 pattern {action},當省略 {action} 時預設動作也是 print $0

printf

printf 語法
printf format [, argument-list]

format (格式) 為字串,argument-list 是對應於 format 的參數列表。

格式佔位字元語法 (format placeholder)
% [parameter] [flags] [width] [.precision] type

parameter (參數)

N$

列印第 N 個參數,如 printf "%2$s %1$s", "world", "hello" 則輸出 hello world

flags (修飾字元)

- (減號)

左對齊,不足寬度時在右邊填滿空格,如 printf "%-5s", "cat" 則輸出 cat  

+ (加號)

用於數字;在正數前面增加一個 + 號。如 printf "%+d", 3 則輸出 +3,而負數則照樣輸出成 -3

space

用於數字;在正數前面增加一個空格 (space),主要是用於跟負數對齊。如 printf "% d", 3 則輸出  3,而負數則照樣輸出成 -3

0

用於數字;不足寬度時在左邊填滿 0,如 printf "%03d", 3 則輸出 003,而負數則輸出成 -03

' (單引號)

用於數字;加上千位分隔符號;支援千位分隔表示的 locale 才有作用。如 printf "%\x27d", 1234 則輸出 1,234

由於 ' (單引號) 需要轉譯,以 16 進制 \x27 避開。以下列示處理 ' (單引號) 的方式:
awk 'BEGIN{printf "%\x27d\n", 1234}' &&\
awk 'BEGIN{printf "%\047d\n", 1234}' &&\
awk 'BEGIN{printf("%'"'"'d\n", 1234)}' &&\
awk "BEGIN{printf \"%'d\n\",1234}"

#

類型 (type) 為 oxX 時,如 %#o 前置 0%#x 前置 0x%#X 前置 0X
類型為 gG 時,如 %#g %#G,補上尾端的 0
當參數的數值為 0 時,只會顯示成 0,並不會「前置」或「補上尾端」。

width (寬度)

「寬度」指定輸出的最少字元數,通常用於固定寬度的輸出。
寬度不足時左邊填滿空格 (右對齊),超出寬度則全部列印,一般應用於數字,如 printf "%5d", 123 則輸出   123
應用於字串如 printf "%5s", "cat" 則輸出   cat

「寬度」可以省略,或者當作為另一個參數傳遞時由星號 * 指示的動態值。 如 printf "%*d", 5, 123 將列印總寬度為 5 個字元的   123

precision (精度)

「精度」通常指定輸出的最多字元數,在不同 type (類型) 其義意不同:
  • 整數類型 (d,i,o,u,x,X) 的位數。

  • 浮點類型 (e,E,f,F) 限制小數點位數。

  • 通用浮點型 (g,G) 為有效位數 (整數加小數位數的總位數)。

  • 字串類型 (s) 限制輸出的最大長度,超過的字串會被截斷。
    printf "%.2s","here" 則輸出 he

「精度」可以省略,或者當作為另一個參數傳遞時由星號 * 指示的動態值。 如 printf "%.*s", 2, "here" 則輸出 he

type (類型)

c

字元,參數為數字如 65 則輸出 A,若為字串則輸出第一個字元。

d, i

整數,無條件捨去法。

e, E

以科學記號顯示浮點數,小寫 e 科學記號顯示為小寫,大寫 E 則為大寫,銀行進位法。

f

浮點數值格式化成定點 (fixed-point)。

g, G

通用浮點型,自動顯示成「浮點 (定點) 類型」或「科學記號類型」,整數位數已經超過由「精度」指定的有效位數,則會顯示成科學記號。小寫 g 科學記號顯示為小寫 e,大寫 G 則為 E,銀行進位法。

o

8 進制。

s

字串。

x, X

16 進制,%x 輸出小寫 af%X 則輸出大寫。

%

輸出一個 % 字元 (第一個格式佔位字元是 %,含佔位字元為兩個 %%)。

字元轉譯

\a

Alert (警報) 嗶聲 (beep), Ctrl+g,ASCII code 7 (BEL).

\b

倒退键 Ctrl+h,ASCII code 8 (BS).

\t

製表字元,TabCtrl+i,ASCII code 9 (HT).

\n

換行字元,Ctrl+j,ASCII code 10 (LF).

\v

垂直製表字元,Ctrl+k,ASCII code 11 (VT).

\f

換頁字元,Ctrl+l,ASCII code 12 (FF).

\r

確認鍵,Ctrl+m,ASCII code 13 (CR).

\nnn

8 進制值 nnn,其中 nnn 為 07 之間的 1 到 3 位數字。

\xhh…​

16 進制數值 hh(0-9A-Fa-f)。

\\

輸出一個 \ 字元。

\"

在字串中輸出 " 雙引號,如 print "Say \"Hello!\" to the world"

數學函數

數學函數

x % y

x 除以 y 的餘數。

x ^ y

x 的 y 次方

sin(x)

正弦

cos(x)

餘弦

atan2(y, x)

反正切

exp(x)

指數

log(x)

對數

sqrt(x)

開根號

int(x)

整數值 (無條件捨去小數)

srand([x])

初始化亂數 rand()x 為亂數種子 (seed),若省略,則以執行時的日期和時間為種子。將種子設置為相同的值,則會產生相同的亂數。

rand()

回傳亂數 n,0 <= n < 1。

位元操作函數

and(v1, v2 [, …])

回傳位元運算 and

compl(val)

回傳 val 位元運算的補數 (complement),將 01 互換。

lshift(val, count)

回傳 val 以位元 (bit) 左移 count 位,(左邊) 高位拋棄,(右邊) 低位補 0。

or(v1, v2 [, …])

回傳位元運算 or

rshift(val, count)

回傳 val 以位元 (bit) 右移 count 位,(左邊) 高位補 0,(右邊) 低位拋棄。

xor(v1, v2 [, …])

回傳位元運算 xor

字串函數

asort(source [, dest [, how ] ])

source 的「內容值」排序。排序後的陣列主鍵由整數 1 開始,陣列內容為「內容值」。如果有指定 dest 則排序結果會儲存在 dest (source 不變)。內建變數 IGNORECASE 會影響排序

asorti(source [, dest [, how ] ])

source 的「主鍵值」排序,排序後的陣列主鍵由整數 1 開始,陣列內容為「主鍵值」。如果有指定 dest 則排序結果會儲存在 dest (source 不變)。內建變數 IGNORECASE 會影響排序

gensub(regexp, replacement, how [, source])

將輸入的 source 匹配 regexp 替換為 replacement 回傳替換後的文字。 未指定 source 參數其來源為欄位 $0,how 設定為數字 n 表示第 n 次開始替換,若為 gG 則為全部替換。regexp 可以是 "字串"/正規表示法/

index(in, find)

回傳字串 findin 的位置 (不支援正規表示法)。

length [(string)]

回傳字串長度,如果沒有 string 參數則回傳 $0 的長度。

match(string, regexp [, array])

取得 string 匹配 regexp 的位置和長度,將開始位置填入內建變數 RSTART,長度填入內建變數 RLENGTHregexp 可以是 "字串"/正規表示法/

array 為正規表示法中分組功能的回撥
echo 'This is an apple and that is an orange.' | awk '{match($0, /an ([^ ]+).+ an ([^.]+)/, arr); print arr[1], arr[2]}'

patsplit(string, array [, fieldpat [, seps ] ])

字串 string 匹配 fieldpat 模式,將合乎模式匹配的文字建立陣列 array,如省略 fieldpat 預設為內建變數 FPATfieldsep 可以是 /正規表示法/"字串",當匹配的文字相同時則陣列主鍵會重複。

split(string, array [, fieldsep [, seps ] ])

字串 string 分割後 (的前後) 建立陣列 array,如省略 fieldsep 預設為內建變數 FS 欄位分隔字元,fieldsep 可以是 "字串"/正規表示法/ 或內部變數 SUBSEP (陣列分隔字元),當分割後的字串相同時則陣列主鍵會重複。

sprintf(format [, argument-list])

回傳(不列印)printf (使用相同參數) 輸出的字串。

strtonum(str)

str 轉成數字 (含小數),若 str 前置 0x (x 可為大寫) 則由 16 進制轉成 10 進制。str 若前置部份數字 (含小數) 則會回傳部份數字,無法轉換時回傳 0。
awk 4.1.4 已不支援前置 0 時轉換成 8 進制。

sub(regexp, replacement [, target])

target 匹配 regexp 替換一次成為 replacement,替換後的文字取代 target。沒有指定 target 參數時預設為欄位變數 $0regexp 可以是 "字串"/正規表示法/

gsub(regexp, replacement [, target])

gsub 跟 sub 類似,sub 只替換一次,gsub 為全部替換。

substr(string, start [, length ])

回傳字串 string 開始位置 start 長度 length,沒有指定 length 則至 string 的結尾。

tolower(string)

轉成小寫

toupper(string)

轉成大寫

輸入輸出函數

close(target [, how ])

輸入的檔案或管道,在經過讀取後,其讀取的位置會增加,如果不關閉輸入,則後續的讀取是接續上次的位置。
關閉檔案 (target) 或者關閉 shell 命令 (target) 的管道,從頭讀取。

在雙向管道 (two-way pipe) 或稱為 coprocess (協助) 可以指定 how 參數,from 為關閉管道的輸入,to 為關閉管道的輸出 。

closetarget 不存在或其他錯誤,並不會出錯只會回傳非零值。發生錯誤時會設定 ERRNO 變數來描述錯誤發生的原因。

fflush([target])

將輸出檔案或管道的緩衝區排清 (flush) 確定已寫入。 flush 出錯時回傳非零值,發生錯誤時會設定 ERRNO 變數來描述錯誤發生的原因。
一般的命令會有緩衝區用於處理資料,當命令已接收到足夠的資料時才會處理。但有時可能需要強制命令處理緩衝區的資枓,以 fflush 函數排清命令的緩衝區。

system(command)

執行操作系統的命令 command 回傳命令的狀態碼。

[command | ] getline [target] [ < file]

讀取下一筆記錄儲存至 target 並回傳讀取狀態,沒有指定 target 時將儲存至 $0 並解析欄位。

執行 getline 錯誤時並不會出錯,而是回傳狀態:
  • 1 正確讀取記錄。

  • 0 檔案結束 (EOF)。
    註:如果是管道輸入,表示管道沒有正確傳送「標準輸出」,可能是檔案不存在,「標準輸出」傳送 EOF

  • -1 錯誤,檔案無法開啟。

  • -2 錯誤,如果是設定重試 PROCINFO[command, "RETRY"] = 1,當發生逾時 (可能) 回傳 -2。

  • 發生錯誤時會設定 ERRNO 變數來描述錯誤發生的原因。

採用 getline < file,由檔案 file 讀取資料,file字串型態
使用 command | getline,將 command (命令) 的輸出管道傳輸到 getlinecommand字串型態

為什麼要強調是字串型態?

因為很容易寫成 getline < file,awk 會將 file 解讀成變數,正確應該為 getline < "file"。當 file 是以變數來組合時,例如 getline < dir "/" file 並不明確,應使用括號包圍如 getline < (dir "/" file)

取得 IP 位置的名稱
getent hosts 8.8.8.8
8.8.8.8         dns.google
getline 取得 IP 位置的名稱
echo -e '8.8.8.8\n8.8.4.4' | awk '
{
  "getent hosts " $0 | getline Hosts;
  split(Hosts, Hosts_Arr, " ");
  print $0, Hosts_Arr[2]
}'
8.8.8.8 dns.google
8.8.4.4 dns.google
列出使用者帳戶、UID 及 GID
awk  -F: '
{
  User = $1
  UID = $3
  while ( (getline < "/etc/group") > 0 )
  {
    if (User ==  $1) {
      print $1, UID, $3
      break
    }
  }
  close("/etc/group") (1)
}' /etc/passwd
1 關閉檔案後,第二次才能從頭開始讀取。

註:本例是為了說明 close,並未最佳化,在 BEGIN{} 時先將 /etc/group 全部讀取至陣列再以陣列查表,比較理想。

print printf 重定向輸出
printprintf 的管道輸出字元其運作跟管道一樣。
print > "filename"
print >> "filename"
print | "command"
print |& "command_coprocess"
將輸出檔案錯誤視為非嚴重錯誤

PROCINFO["NONFATAL"] 或者 PROCINFO["filename", "NONFATAL"] 存在時 (不管項目設定值,設為 0 也一樣),則 print 重定向至檔案的 I/O 錯誤將變為非嚴重錯誤 (註:awk 5.0 才會作用)。

awk '
BEGIN {
  PROCINFO["/no/such/file", "NONFATAL"] = 0 (1)
  ERRNO = 0
  print "hi" > "/no/such/file"
  if (ERRNO) {
    print("Output failed:", ERRNO) > "/dev/stderr"
    exit 1
  }
}'
1 設定為 0 是在解釋該項目存在時即有作用,實際上不需要設定;或者設定為 1。
Output failed: No such file or directory
雙向管道

將管道的輸入及輸出;由 print 傳送給命令的「管道輸入」;由 getline 讀取命令的「管道輸出」。

|& 建立雙向管道
print |& command
command |& getline x

||& 可能混淆以為行為一樣,| command 是由命令直接輸出,而 |& command 其命令是回傳給 awk,命令並不會輸出。

依據命令的特性,如命令是以逐行來處理,則每傳送一筆記錄後執行 close(command, "to") 在管道中寫入 EOF,避免要求讀取所有資料後才會處理的「命令」發生阻塞 (永久等待)。
傳送後執行關閉管道的輸出 close(command, "to"),在執行 getline 之後也需要執行 close(command),這兩個 close 是成對的。

{
  print |& command
  close(command, "to") (1)

  if ( (command |& getline x) > 0 )
     print x

  close(command) (2)
}
1 close(command, "to") 在管道中寫入 EOF
2 在處理後需要執行 close(command),這兩個 close 是成對的。

如果命令是讀取所有的輸入後才處理 (或者是讀取全部後才會正確),那麼則分段;先傳送所有記錄後,再傳送 EOF,再以 getline (全部) 讀取。

{
  print |& command (1)
}

END {
  close(command, "to") (2)
  while ( (command |& getline x) > 0) {
    print x
  }
  close(command) (3)
}
1 先傳送所有記錄。
2 傳送所有記錄後,再傳送 EOF
3 實際上並不需要該指令,但加入該指定比較合乎 close 的用法。
將使用者帳戶轉成大寫 (逐行處理)
awk  -F: '
BEGIN {
  command="awk \x27{ print toupper($0) }\x27" (1)
}
{
  print $1 |& command
  close(command, "to")

  if ( (command |& getline x) > 0 )
     print x
  close(command)
}
' /etc/passwd
1 command 即為 awk '{ print toupper($0) }',將 ' (單引號) 以 16 進制 \x27 避開轉譯。
將使用者帳戶經過 cat -n 加上行號,讀取後再列印 (全部資料分段處理)。
awk  -F: '
BEGIN {
  command="cat -n"
}

{
  print $1 |& command
}

END {
  close(command, "to")
  while ( (command |& getline x) > 0 )
    print x
  close(command)
}
' /etc/passwd
逾時測試
awk '
BEGIN {
  command = "echo hi"
  print "getline return:" (command | getline)
  print "close return:" close(command)

  print "----------"
  command = "sleep 1 && echo hi"
  PROCINFO[command, "RETRY"] = 1
  PROCINFO[command, "READ_TIMEOUT"] = 200
  print "getline return:" (command | getline), "ERRNO:" ERRNO
  print "close return:" close(command), "ERRNO:" ERRNO
  print "close return:" close(command), "ERRNO:" ERRNO
}'
getline return:1
close return:0
----------
getline return:-2 ERRNO:Connection timed out (1)
close return:269 ERRNO: (2)
close return:-1 ERRNO:close of redirection that was never opened
1 不一定是 -2,awk 4.1.4 為 -1。
2 不一定是 269 可能是其他非 0 的數字。

awk 關聯陣列

陣列主鍵值為唯一,陣列主鍵 (及內容值) 可為文字或數字,收集或統計欄位的情況。

陣列類型
  • array[x,y] 多主鍵陣列
    水果有多種顏色,水果及顏色為同一組資料時,可採用多主鍵陣列。

  • array[x][y] 多維陣列
    水果有多種顏色,如要各別列出水果、顏色,處理不同的維度時,應採用多維陣列。

多主鍵陣列是儲存在一維陣列,其主鍵是以 SUBSEP (陣列分隔字元) 來分隔,該字元即為 Ctrl+\
awk '
BEGIN {
  a[1,1] = 11;
  a[1,2] = 12;
  a[2,2] = 22;
  for (i in a)
    print i
}
' | cat -v
2^\2
1^\1
1^\2
多維陣列,在程式中可取得一個維度,其數量為該維度的個數。
awk '
BEGIN {
  a[1][1] = 11;
  a[1][2] = 12;
  a[2][2] = 22;
  for (x in a) {
    print x
    for (y in a[x])
      print "\t" y
  }
}
'
1
        1
        2
2
        2
建立銷售範例資料
cat > sales.txt << EOF
sales,fruit,color,qty,amount
john,apple,red,1,100
john,banana,yellow,2,200
john,kiwi,green,3,300
kevin,lemon,yellow,1,100
marry,banana,yellow,2,200
marry,apple,green,3,300
EOF
統計水果及業務員銷售金額
awk -F, '
{
  if (NR < 2) next
  fruitAmount[$2] += $5
  salesAmount[$1] += $5
}
END {
  print "---------- Fruit amount"
  for (fruit in fruitAmount) print fruit, fruitAmount[fruit]

  print "---------- Sales amount"
  for (sales in salesAmount) print sales, salesAmount[sales]
}
' sales.txt
程式說明:
  1. if (NR < 2) next 目前行號 (NR) 小於 2 時,執行下一行 next

  2. fruitAmount[$2] += $5 水果銷售陣列 fruitAmount 其主鍵為水果欄位 $2,內容為累加銷售金額 $5
    寫成 fruitAmount[$2] = fruitAmount[$2] + $5 也相同。

  3. for (fruit in fruitAmount)for 取得 fruitAmount 的每個主鍵 fruit (水果欄位)。

  4. fruitAmount[fruit] 以主鍵 fruit 取得 fruitAmount 銷售金額內容值。

---------- Fruit amount
apple 400
banana 400
lemon 100
kiwi 300
---------- Sales amount
kevin 100
marry 500
john 600
預先定義的陣列排序
陣列排序 @[ind|val]_[num|type|str]_[asc|desc] 選項
  • @unsorted 無排序。

  • ind 按主鍵(索引)排序,val 按內容值排序。

  • num 嘗試以數字排序 (1a 會以 1 排序),無前置數字在前。
    str 按字串 (文字) 順序排序。
    type 按類型排序,數字類型或類數字 (內容全部為數字) 在前,字串 (文字) 類型在後 (1a 為字串類型)。

  • asc 為升冪,desc 為降冪。

陣列排序組合列表

@ind_num_asc @ind_num_desc @ind_str_asc @ind_str_desc @val_num_asc @val_num_desc @val_str_asc @val_str_desc @val_type_asc @val_type_desc
註:沒有 ind_type 的組合。

使用方式
PROCINFO["sorted_in"] = "@ind_str_asc"
for (i in array) print i, array[i]
str num type 排序示例
awk '
function sortdemo(format, arr)
{
  PROCINFO["sorted_in"] = format (4)
  print format
  for (i in arr) {
    print typeof(arr[i]), arr[i] (5)
  }
  print "-------------"
}
BEGIN {
  split("31 5 red 1a", a) (1)
  a[100] = 400 (2)
  sortdemo("@val_str_asc", a) (3)
  sortdemo("@val_num_asc", a)
  sortdemo("@val_type_asc", a)
}
'
1 建立陣列 a 內容值為 315red1a
2 陣列 a[100] 設定為數字 400。
3 將排序方式 @val_str_asc 傳遞給 sortdemoformat
4 sortdemo 設定排序方式 format
5 列印每個陣列的資料類型及內容 (awk 4.2 才有支援 typeof 函數)。
@val_str_asc
string 1a
strnum 31
number 400
strnum 5
string red
-------------
@val_num_asc
string red
string 1a
strnum 5
strnum 31
number 400
-------------
@val_type_asc
strnum 5
strnum 31
number 400
string 1a
string red
-------------
統計水果銷售明細 (多主鍵陣列)
awk -F, '
{
  if (NR < 2) next
  fruitQty[$2,$3] += $4
  fruitAmount[$2,$3] += $5
}
END {
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (combKey in fruitQty) {
    split(combKey, sepKey, SUBSEP)
    print sepKey[1], sepKey[2], fruitQty[sepKey[1], sepKey[2]], fruitAmount[sepKey[1], sepKey[2]]
  }
}
' sales.txt
程式說明:
  1. for (combKey in fruitQty)for 取得 fruitQty 的每個主鍵 combKey

  2. split(combKey, sepKey, SUBSEP)
    由於 (原始的) fruitQty 為多主鍵,combKey 也是多主鍵,執行 splitSUBSEP (陣列分隔字元) 分割出每個主鍵儲存至陣列 sepKey 中的 sepKey[1]sepKey[2]

  3. fruitQty[sepKey[1], sepKey[2]] 以主鍵 sepKey[1]sepKey[2] 取得 fruitQty 的內容值,fruitAmount 的主鍵跟 fruitQty 相同,以相同的主鍵 fruitAmount[sepKey[1], sepKey[2]] 取得 fruitAmount 的內容值。

水果銷售明細結果
apple green 3 300
apple red 1 100
banana yellow 4 400
kiwi green 3 300
lemon yellow 1 100
統計水果銷售明細 (asorti 示例)
awk -F, '
{
  if (NR < 2) next
  fruitQty[$2,$3] += $4
  fruitAmount[$2,$3] += $5
}
END {
  asorti(fruitQty, sortKey)
  for (i=1; i in sortKey; i++) {
    split(sortKey[i], sepKey, SUBSEP)
    print sepKey[1], sepKey[2], fruitQty[sepKey[1], sepKey[2]], fruitAmount[sepKey[1], sepKey[2]]
  }
}
' sales.txt

執行結果跟前述的「水果銷售明細結果」相同。

程式說明:
  1. asorti(fruitQty, sortKey)fruitQty 的主鍵排序後儲存至 sortKey 其主鍵值由整數 1 開始。

    sortKey 陣列如下:
    1 apple^\green
    2 apple^\red
    3 banana^\yellow
    4 kiwi^\green
    5 lemon^\yellow
  2. for (i=1; i in sortKey; i++)for 取得每個整數主鍵 iforconditioni in sortKey 改為 i <= length(sortKey) 也相同。

    for (i=1; i <= length(sortKey); i++)
  3. split(sortKey[i], sepKey, SUBSEP)
    sortKey[i] 為 (已排序的) 多主鍵的內容,執行 splitSUBSEP (陣列分隔字元) 分割出每個主鍵儲存至陣列 sepKey 中的 sepKey[1]sepKey[2]

  4. fruitQty[sepKey[1], sepKey[2]] 以主鍵 sepKey[1]sepKey[2] 取得 fruitQty 的內容值,fruitAmount 的主鍵跟 fruitQty 相同,以相同的主鍵 fruitAmount[sepKey[1], sepKey[2]] 取得 fruitAmount 的內容值。

水果銷售要統計出小計,以多維陣列來實作比較簡單
awk -F, '
{
  if (NR < 2) next
  fruitSales[$2][$3]["qty"] += $4
  fruitSales[$2][$3]["amt"] += $5
}
END {
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (fruit in fruitSales) {
    subqty = 0; subamt = 0;
    for (color in fruitSales[fruit]) {
      qty = fruitSales[fruit][color]["qty"]
      amt = fruitSales[fruit][color]["amt"]
      print fruit, color, qty, amt
      subqty += qty; subamt += amt;
    }
    print "--------------------"
    print fruit, "subtotal", subqty, subamt
    print ""
  }
}
' sales.txt
apple green 3 300
apple red 1 100
--------------------
apple subtotal 4 400

banana yellow 4 400
--------------------
banana subtotal 4 400

kiwi green 3 300
--------------------
kiwi subtotal 3 300

lemon yellow 1 100
--------------------
lemon subtotal 1 100
將水果按顏色分類 (多維陣列)
(
cat << EOF
fruit,color-list
apple,green,red
lemon,green,yellow
banana,yellow
kiwi,green,gold
grape,black,red,purple
EOF
) |
awk -F, '
{
  if (NR < 2) next
  for(i=2; i<=NF; i++)
    array[$i][$1]
}
END {
  PROCINFO["sorted_in"] = "@ind_str_asc"
  for (color in array) {
    print color, length(array[color])

    for (fruit in array[color]) {
      print "\t" fruit
    }
  }
}
'
程式說明:
  1. for(i=2; i<=NF; i++)for 取得 i,該值跳過第 1 欄位的水果,由 2 開始至該行的欄位數 NF,i 值為顏色欄位。

  2. array[$i][$1] 收集多維陣列 array 第 1 維度的主鍵為顏色欄位 $i,第 2 維度的主鍵為水果 $1
    註:只需收集主鍵值,不需要內容值。

black 1
        grape
gold 1
        kiwi
green 3
        apple
        kiwi
        lemon
purple 1
        grape
red 2
        apple
        grape
yellow 2
        banana
        lemon