pslaboが試したことの記録

はてなダイヤリーからはてなブログに引っ越してきました

この日記は現在実行中の減量記録を含む個人的なメモとして始めましたが、最近はコンピュータやガジェット、ハック、セキュリティネタのほうがメインになっております。

はてなダイヤリー時代はカテゴリ分けが適当だったのですが、これはそのうち直します。


awkの機能だけでシェル変数を参照するテスト

※一部、誤解に基づく記述がありましたので、内容や試験コードを改修しています。

awkからはシェル変数を参照できない、というのは常識です。だから間違った理解なのですが、awk にシェル変数を渡す場合には、"-v 変数名=値" でコマンドラインパラメータとして渡したり、あるいは ワンライナーの場合は awkスクリプトの中にシェル変数を直接展開するという方法はバッドノウハウ的に案外用いられています。

しかしこういうやり方は少々強引なバッドノウハウだと思います。ですが色々思案するうちに「awk 内から set を実行した結果を getline で読み込んで、これをハッシュ配列に格納したらええんじゃないか?」と思い立ったので書いてみました。

このサンプルコードでは、ハッシュ配列 _env にシェル変数が代入されるようにしてみました。
着想5分、実装15分程度の手抜きコードだけど、それなりには動きます。

awk からはハッシュ変数 ENVIRON でシェル変数を参照できるのですが、実は ENVIRONでは取得できない値もあります。そこで「外部コマンドとして set を実行した結果を getline で読み込む場合」と「ENVIRON で値を取得した場合」の2種類の結果を比べてみることにしました。

このコードは MacOSX Yosemite 上の awk、および brew でインストールした gawk 4.1.3 で動作確認しました。


まずは本件の試験コード。

#!/usr/bin/awk -f

# シェル変数を awk から取得するために、awk から set を実行した戻り値をハッシュ配列 _env に代入します。
# using getline to get shellenv by executing "set" command, and set values into hash array "_env"
function get_shellenv( CMD, FS_BACKUP, i ) {
    CMD="set" 
    FS_BACKUP=FS

    # 取得したシェル変数の一覧は "=" でトークン分割するので FS を一時的に変更する。
    # set FS with temporary to split key and value for each line on shellenv.
    FS="="
    while ( CMD | getline ) {
        _env[$1]=$2
        
        # NF > 2 の場合は $2 〜 $NF を "=" でつなげます。
        for ( i = 3  ; i <= NF ; i++ ) {
            _env[$1]=_env[$1] "=" $i
        }
    }
    # FS を元の値に戻します。
    FS=FS_BACKUP
}


BEGIN {
    # スクリプトを実行開始するときに get_shellenv を呼んでおけばこのスクリプトでは _env[] でシェル変数が参照できます。
    # If call this function with begin block, shell env can references by _env[]
    get_shellenv()


    # 実際にハッシュ配列に代入された値をチェックする処理
    # check all the sehellenv key and value.    
    for ( i in _env ) {
        countenv[i]++
        count_env[i]++
    }

    # ENVIRON に保存されているシェル変数をチェックする処理
    # check all the ENVIRON key and value
    for ( i in ENVIRON ) {
        countenv[i]++
        count_ENVIRON[i]++
    }

    # 2種類の処理で取得できるシェル変数を表示しつつ、内容の一致度を確認する処理
    # display all the shell environment, and check key or value will be match or not with both method
    for ( i in countenv ) {

        # 2種類の方法で同じ値が取得できるケース
        # match values with both methdo
        if ( countenv[i] == 2 && _env[i] == ENVIRON[i] ) {
            printf "[same] %s=%s\n", i, _env[i] | "sort"
        } else {
            # ENVIRON だけで参照できるシェル変数
            # get value only ENVIRON
            if ( count_ENVIRON[i] == 1 && count_env[i] == 0 ) {
                printf "[ENVIRION] %s=%s\n", i, ENVIRON[i] | "sort"
            }

            # ENVIRON では参照できないシェル変数
            # cannot get by ENVIRON
            if ( count_ENVIRON[i] == 0 && count_env[i] == 1 ) {
                printf "[_env] %s=%s\n", i, _env[i] | "sort"
            }

            # 両方の方法で値を参照できるけれど、値が一致しないケース
            # get both method, but value is not match.
            if ( _env[i] != "" && ENVIRON[i] != "" ) {
                printf "[mismatch] %s=%s (_env)\n",    i, _env[i] | "sort"
                printf "[mismatch] %s=%s (ENVIRON)\n", i, ENVIRON[i] | "sort"
            }
        }
    }
}


そしてこの試験実装の実行例は以下のとおりです。なお、スクリプトの実行前に2つのシェル変数を試験用に設定しています。

  • _env と表記されている行は ENVIRON では値が取得できず、set の実行結果だけで参照できるシェル変数です。この中で参照したくなりそうな値は UID, GUID, HOSTNAME あたりでしょう。これらの値を参照したい場合は ENVIRON 以外の方法を検討せねばなりません。
  • mismatch は、_env と ENVIRON で結果に微妙な差異が出るケースです。ただしこの差異は実質的に問題にはならないはずです。SHLVLの値が1違うのは、set の実行は子プロセスが増えるためなので問題なし。もう一つの差異は代入値に空白を含むケースですが、この場合は ENVIRON の値のほうが使いやすいはずでしょう。
  • same は、_env と ENVIRON の結果が完全に一致するケースです。
$ export testenv_with_blank='aaa bob'
$ export testenv_no_blank=aaabbb
$ ./test.awk
[_env] '=
[_env] BASH=/bin/sh
[_env] BASH_ARGC=()
[_env] BASH_ARGV=()
[_env] BASH_EXECUTION_STRING=set
[_env] BASH_LINENO=()
[_env] BASH_SOURCE=()
[_env] BASH_VERSINFO=([0]="3" [1]="2" [2]="57" [3]="1" [4]="release" [5]="x86_64-apple-darwin15")
[_env] BASH_VERSION='3.2.57(1)-release'
[_env] DIRSTACK=()
[_env] EUID=501
[_env] GROUPS=()
[_env] HOSTNAME=somehostname.local
[_env] HOSTTYPE=x86_64
[_env] IFS=' 	
[_env] MACHTYPE=x86_64-apple-darwin15
[_env] OPTERR=1
[_env] OPTIND=1
[_env] OSTYPE=darwin15
[_env] POSIXLY_CORRECT=y
[_env] PPID=75970
[_env] PS4='+ '
[_env] SHELLOPTS=braceexpand:hashall:interactive-comments:posix
[_env] UID=501
[mismatch] SHLVL=1 (ENVIRON)
[mismatch] SHLVL=2 (_env)
[mismatch] testenv_with_blank='aaa bbb' (_env)
[mismatch] testenv_with_blank=aaa bbb (ENVIRON)
[same] Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.srzAZCImzI/Render
[same] DISPLAY=/private/tmp/com.apple.launchd.nS2YMWGQyX/org.macosforge.xquartz:0
[same] HOME=/Users/__USERNAME__
[same] LANG=ja_JP.UTF-8
[same] LOGNAME=__USERNAME__
[same] PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/MacGPG2/bin:/Users/__USERNAME__/bin
[same] PWD=/Users/__USERNAME__
[same] SHELL=/bin/bash
[same] SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.ADCjS4qvtW/Listeners
[same] TERM=xterm-256color
[same] TERM_PROGRAM=Apple_Terminal
[same] TERM_PROGRAM_VERSION=361.1
[same] TERM_SESSION_ID=350DA08B-BD12-4A2C-BF24-6D39F0F472EB
[same] TMPDIR=/var/folders/t5/1b993tfs463cn36v8vvmkrcr0000gn/T/
[same] USER=__USERNAME__
[same] XPC_FLAGS=0x0
[same] XPC_SERVICE_NAME=0
[same] _=./test3.awk
[same] __CF_USER_TEXT_ENCODING=0x1F5:0x1:0xE
[same] testenv_no_blank=aaabbb
$