Mazn.net

やってみて 調べてみて 苦労しなけりゃ 箱は動かじ

bash

bashの変数のスコープまとめ

bash-scope



たまにbashスクリプトを書くと忘れてしまってるので、bashの変数のスコープについてのメモ。

  1. 通常の変数はシェル変数と呼ばれ、関数外でも関数内でも宣言可
    1. シェル変数のスコープはグローバル変数となり、同スクリプト内(同プロセス内)のどこからでも参照・変更できる
      • 関数内でdeclareを使って変数を宣言する場合、グローバルにするには -g オプションが必要
      • 別のスクリプトファイルであっても、sourceコマンドで読み込んだ(呼び出した)場合は、そのプロセスは呼び出し元のスクリプト内に記述された事と同等の扱いとなる
    2. サブシェル として呼び出した場合、サブシェル内で同変数の参照はできるが、変更しても呼び出し元の変数には影響を与えない
      • 呼び出し時にパイプを使用すると、サブシェル内で実行されるので注意
    3. 別のスクリプト(別プロセス)を呼び出した場合、呼び出した先のスクリプト内で呼び出し元のシェル変数は参照も変更もできない
  2. local宣言された変数はローカル変数と呼ばれ、関数内でのみ宣言可
    1. 関数内で宣言されたローカル変数は、その関数内および同スクリプト内の呼び出し先関数内で自由に参照・変更ができるが、呼び出し先にない関数からは参照も変更もできない
      • 「ローカル変数はその関数内でのみ有効」と説明する人もいるが、ローカル変数であっても、同関数内で呼び出した先の関数内では参照・変更できるため注意。bashではその関数内だけに完全に閉じた変数を宣言する機能は今(ver5)のところなさそう。
    2. サブシェルとして呼び出した場合、サブシェル内で同変数の参照はできるが、変更しても呼び出し元の変数には影響を与えない
      • 呼び出し時にパイプを使用すると、サブシェル内で実行されるので注意
    3. 別のスクリプト(別プロセス)を呼び出した場合、呼び出した先のスクリプト内で呼び出し元のローカル変数は参照も変更もできない
    4. 関数呼び出し元に同名の変数が宣言済みの場合、呼び出し元の同名の変数の値は参照も変更もできなくなる
      • ただしbash 5.0以降では、shopt -s localvar_inherit を設定すると呼び出し元の同名の変数の値が自動で引き継がれ参照できるようになる
  3. export宣言された変数は環境変数と呼ばれる
    1. シェル変数の場合は1-3動作が、ローカル変数の場合は2-3の動作が変わり、別のスクリプトを呼び出した場合、呼び出した先のスクリプト内で呼び出し元のシェル変数やローカル変数を参照できるようになる。ただし変更はできない
      • ただし配列や連想配列は、exportしても参照できない (exportできない)

以下のテストスクリプトを使って動作確認してみました。実行環境のbashのバージョンは5.1.3です。

bash-scope-test.sh

#!/bin/bash

primal_function() {
	declare -g -x export_global="primal value"
	local -x export_local="primal value"
	declare -g -x -a export_global_array=("primal" "value")
	local -x -a export_local_array=("primal" "value")
	declare -g global="primal value"
	declare -g -a global_array=("primal" "value")
	local local="primal value"
	local -a local_array=("primal" "value")

	echo "---- call function ----"
	called_function
	echo "---- call function (returned) ----"
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"

	# reset
	export_global="primal value"
	export_local="primal value"
	export_global_array=("primal" "value")
	export_local_array=("primal" "value")
	global="primal value"
	global_array=("primal" "value")
	local="primal value"
	local_array=("primal" "value")

	echo
	echo "---- call function as subshell ----"
	(called_function)
	echo "---- call function as subshell (returned) ----"
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"
	# reset
	export_global="primal value"
	export_local="primal value"
	export_global_array=("primal" "value")
	export_local_array=("primal" "value")
	global="primal value"
	global_array=("primal" "value")
	local="primal value"
	local_array=("primal" "value")

	echo
	echo "---- call function as pipe ----"
	called_function | cat
	echo "---- call function as pipe (returned) ----"
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"
	# reset
	export_global="primal value"
	export_local="primal value"
	export_global_array=("primal" "value")
	export_local_array=("primal" "value")
	global="primal value"
	global_array=("primal" "value")
	local="primal value"
	local_array=("primal" "value")

	echo
	echo "---- call other script ----"
	./other-script.sh
	echo "---- call other script (returned) ----"
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"
	# reset
	export_global="primal value"
	export_local="primal value"
	export_global_array=("primal" "value")
	export_local_array=("primal" "value")
	global="primal value"
	global_array=("primal" "value")
	local="primal value"
	local_array=("primal" "value")

	echo
	echo "---- call redefined function ----"
	called_redefined_function
	echo "---- call redefined function (returned) ----"
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"
	# reset
	export_global="primal value"
	export_local="primal value"
	export_global_array=("primal" "value")
	global="primal value"
	global_array=("primal" "value")
	local="primal value"
	local_array=("primal" "value")

	echo
	echo "---- call redefined function as localvar_inherit ----"
	shopt -s localvar_inherit
	called_redefined_function
	echo "---- call redefined function as localvar_inherit (returned) ----"
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"
	shopt -u localvar_inherit
	# reset
	export_global="primal value"
	export_local="primal value"
	export_global_array=("primal" "value")
	export_local_array=("primal" "value")
	global="primal value"
	global_array=("primal" "value")
	local="primal value"
	local_array=("primal" "value")	
}

# caelld from primal_function()
called_function() {
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"

	export_global="**called value**"
	export_local="**called value**"
	export_global_array=("**called" "value**")
	export_local_array=("**called" "value**")
	global="**called value**"
	global_array=("**called" "value**")
	local="**called value**"
	local_array=("**called" "value**")
}

# caelld from primal_function()
called_redefined_function() {
	# local export_global
	# local export_local
	# local -a export_global_array
	# local -a export_local_array
	# local global
	# local -a global_array
	# local local
	# local -a local_array

	declare -g -x export_global
	local -x export_local
	declare -g -x -a export_global_array
	local -x -a export_local_array
	declare -g global
	declare -g -a global_array
	local local
	local -a local_array

	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"

	export_global="**redefined value**"
	export_local="**redefined value**"
	export_global_array=("**redefined" "value**")
	export_local_array=("**redefined" "value**")
	global="**redefined value**"
	global_array=("**redefined" "value**")
	local="**redefined value**"
	local_array=("**redefined" "value**")
}

# called from main()
other_function() {
	echo "       export_global=$export_global"
	echo "        export_local=$export_local"
	echo " export_global_array=${export_global_array[*]}"
	echo "  export_local_array=${export_local_array[*]}"
	echo "              global=$global"
	echo "        global_array=${global_array[*]}"
	echo "               local=$local"
	echo "         local_array=${local_array[*]}"

	export_global="**other value**"
	export_local="**other value**"           # this is not declared, so it becomes global
	export_global_array=("**other" "value**")
	export_local_array=("**other" "value**") # this is not declared, so it becomes global
	global="**other value**"
	global_array=("**other" "value**")
	local="**other value**"                  # this is not declared, so it becomes global
	local_array=("**other" "value**")        # this is not declared, so it becomes global
}

primal_function

echo
echo "---- main() ----"
echo "       export_global=$export_global"
echo "        export_local=$export_local"
echo " export_global_array=${export_global_array[*]}"
echo "  export_local_array=${export_local_array[*]}"
echo "              global=$global"
echo "        global_array=${global_array[*]}"
echo "               local=$local"
echo "         local_array=${local_array[*]}"

echo "---- call other function in main() ----"
other_function

echo "---- call other function in main() (returned) ----"
echo "       export_global=$export_global"
echo "        export_local=$export_local"
echo " export_global_array=${export_global_array[*]}"
echo "  export_local_array=${export_local_array[*]}"
echo "              global=$global"
echo "        global_array=${global_array[*]}"
echo "               local=$local"
echo "         local_array=${local_array[*]}"

other-script.sh

#!/bin/bash

echo "       export_global=$export_global"
echo "        export_local=$export_local"
echo " export_global_array=${export_global_array[*]}"
echo "  export_local_array=${export_local_array[*]}"
echo "              global=$global"
echo "        global_array=${global_array[*]}"
echo "               local=$local"
echo "         local_array=${local_array[*]}"

export_global="**other script value**"
export_local="**other script value**"
export_global_array=("**other" "script value**")
export_local_array=("**other" "script value**")
global="**other script value**"
global_array=("**other" "script value**")
local="**other script value**"
local_array=("**other" "script value**")

実行結果

$ ./bash-scope-test.sh
---- call function ---- ★通常の関数呼び出しは全部参照可能
export_global=primal value
export_local=primal value
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value
---- call function (returned) ---- ★通常の関数呼び出し先では呼び出し元を変更可
export_global=**called value**
export_local=**called value**
export_global_array=**called value**
export_local_array=**called value**
global=**called value**
global_array=**called value**
local=**called value**
local_array=**called value**

---- call function as subshell ---- ★サブシェルでの呼び出しは全部参照可能
export_global=primal value
export_local=primal value
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value
---- call function as subshell (returned) ---- ★サブシェルでの呼び出し先では呼び出し元を変更不可
export_global=primal value
export_local=primal value
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value

---- call function as pipe ---- ★パイプでの呼び出しはサブシェルと同等
export_global=primal value
export_local=primal value
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value
---- call function as pipe (returned) ----
export_global=primal value
export_local=primal value
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value

---- call other script ---- ★他のスクリプト呼び出しではexportされた変数しか参照できない
export_global=primal value
export_local=primal value
export_global_array= ★配列はexportできない
export_local_array= ★配列はexportできない
global=
global_array=
local=
local_array=
---- call other script (returned) ---- ★他のスクリプト内で呼び出し元を変更不可
export_global=primal value
export_local=primal value
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value

---- call redefined function ----
export_global=primal value ★ 呼び出し先で同名のグローバル変数の定義は、呼び出し元の変数がそのまま参照される
export_local= ★ 全く異なる関数内で定義されたローカル変数は参照できない
export_global_array=primal value
export_local_array=
global=primal value
global_array=primal value
local=
local_array=
---- call redefined function (returned) ----
export_global=**redefined value** ★ 呼び出し先で同名のグローバル変数の定義は、呼び出し元の変数がそのまま変更できる
export_local=primal value
export_global_array=**redefined value**
export_local_array=primal value
global=**redefined value**
global_array=**redefined value**
local=primal value
local_array=primal value

---- call redefined function as localvar_inherit ----
export_global=primal value ★localvar_inheritは、グローバルな変数の動作には影響なし
export_local=primal value ★localvar_inheritは、local宣言された変数に呼び出し元の同名の変数の値が代入される
export_global_array=primal value
export_local_array=primal value
global=primal value
global_array=primal value
local=primal value
local_array=primal value
---- call redefined function as localvar_inherit (returned) ----
export_global=**redefined value**
export_local=primal value ★localvar_inheritでも、local宣言された変数は呼び出しもとの同名の変数の値は変更されない
export_global_array=**redefined value**
export_local_array=primal value
global=**redefined value**
global_array=**redefined value**
local=primal value
local_array=primal value

---- main() ----
export_global=primal value ★関数内で定義されたグローバル変数はスクリプト内ではどこからでも参照可能
export_local=
export_global_array=primal value
export_local_array=
global=primal value
global_array=primal value
local=
local_array=
---- call other function in main() ----
export_global=primal value ★関数内で定義されたグローバル変数はスクリプト内ではどこからでも参照可能
export_local=
export_global_array=primal value
export_local_array=
global=primal value
global_array=primal value
local=
local_array=
---- call other function in main() (returned) ----
export_global=**other value**
export_local=**other value** ★other_function()内ではprimal_function()内のlocal変数は参照できないため、直接変数を変更するとグローバルとみなされてしまう
export_global_array=**other value**
export_local_array=**other value**
global=**other value**
global_array=**other value**
local=**other value**
local_array=**other value**

なお関数にlocal宣言はありません。そのため変数と異なり関数内で関数を定義した場合、関数のスコープは全てグローバルとなります。関数はexportでき、変数と同様に呼び出し先のスクリプト内でも使用できます。

シェルスクリプト内でのaliasの展開タイミング

シェルスクリプト(bash)内では、デフォルトでaliasを使用できません。使用するにはshoptコマンドを使って、明示的に有効化する必要があります。

aliastest.sh

#!/usr/bin/bash
shopt -s expand_aliases

alias myecho='echo myecho'
myecho
$ ./aliastest.sh
myecho

ただしシェルスクリプト利用時は、aliasが展開されるタイミングを考慮しないと少しハマります。

aliasはスクリプト読み込み時 (関数の定義読み込み時) に展開されます

これはbashのmanにも以下のように説明されています。

Aliases are expanded when  a  command  is read, not when it is executed.

そのため、関数内でaliasを使用する場合、aliasの定義のタイミングが重要になります。以下のようにmyfunc1()内の後ろでmain_aliasを定義した場合、command not found となります。
※ 以下は、myfunc1()の実行結果が把握しやすいように、sedで整形しています。

aliastest1.sh

#!/usr/bin/bash
shopt -s expand_aliases

myfunc1() {
    echo "----myfunc1()内でmain_alias実行-------------"
    main_alias
    echo "--------------------------------------------"
}

alias main_alias="echo main alias"
myfunc1
$ ./aliastest1.sh
----myfunc1()内でmain_alias実行-------------
./aliastest1.sh: line 6: main_alias: command not found
--------------------------------------------

main_aliasの定義をmyfunc1関数の前に書けば正常に動作します。

aliastest2.sh

#!/usr/bin/bash
shopt -s expand_aliases
alias main_alias="echo main alias"

myfunc1() {
    echo "----myfunc1()内でmain_alias実行-------------"
    main_alias
    echo "--------------------------------------------"
}

myfunc1
$ ./aliastest2.sh
----myfunc1()内でmain_alias実行-------------
main alias
--------------------------------------------

関数の定義読み込み時に展開されるため、関数内でaliasを定義しても
 「その関数は実行されていない = aliasが定義されていない」
ためエラーとなります。

aliastest3.sh

#!/usr/bin/bash
shopt -s expand_aliases
alias main_alias="echo main alias"

myfunc1() {
    echo "----myfunc1()内でmain_alias実行-------------"
    main_alias
    echo "--------------------------------------------"
    myfunc1_alias="echo myfunc1 alias"
    echo "----myfunc1()内でmyfunc1_alias実行----------"
    myfunc1_alias
    echo "--------------------------------------------"
}

myfunc1
$ ./aliastest3.sh
----myfunc1()内でmain_alias実行-------------
main alias
--------------------------------------------
----myfunc1()内でmyfunc1_alias実行----------
./aliastest3.sh: line 11: myfunc1_alias: command not found
--------------------------------------------

また関数内でのaliasの定義は、同じシェル内で実行されていれば実行元の関数にも反映されます。

aliastest4.sh

#!/usr/bin/bash
shopt -s expand_aliases
alias main_alias="echo main alias"

echo "---- main()内で定義されているalias一覧 1--------"
alias
echo "------------------------------------------------"

myfunc1() {
    alias myfunc1_alias="echo myfunc1 alias"
}

myfunc1

echo "---- main()内で定義されているalias一覧 2--------"
alias
echo "------------------------------------------------"
$ ./aliastest4.sh
---- main()内で定義されているalias一覧 1--------
alias main_alias='echo main alias'
------------------------------------------------
---- main()内で定義されているalias一覧 2--------
alias main_alias='echo main alias'
alias myfunc1_alias='echo myfunc1 alias'
------------------------------------------------

スコープはシェル変数と同じのようです。( ) やパイプを使ってサブシェルで実行したり、他のファイルを実行した場合(子プロセスの場合)にも引き継ぐことはできません。
※ パイプを使うと、パイプの左の式もサブシェルで実行されます

aliastest5.sh

#!/usr/bin/bash
shopt -s expand_aliases
alias main_alias="echo main alias"

myfunc1() {
    alias myfunc1_alias="echo myfunc1 alias"
}

myfunc1 | true
echo "---- main()内で定義されているalias一覧 1--------"
alias
echo "------------------------------------------------"

(myfunc1)
echo "---- main()内で定義されているalias一覧 2--------"
alias
echo "------------------------------------------------"

./aliastest5_sub.sh
echo "---- main()内で定義されているalias一覧 3--------"
alias
echo "------------------------------------------------"

aliastest5_sub.sh

#!/usr/bin/bash
shopt -s expand_aliases

alias sub_alias="echo sub alias"
$ ./aliastest5.sh
---- main()内で定義されているalias一覧 1--------
alias main_alias='echo main alias'
------------------------------------------------
---- main()内で定義されているalias一覧 2--------
alias main_alias='echo main alias'
------------------------------------------------
---- main()内で定義されているalias一覧 3--------
alias main_alias='echo main alias'
------------------------------------------------

子プロセスとして./aliastest5_sub.sh で呼び出すのではなく source コマンドを使って実行すれば、サブシェルにならないため alias を引き継ぐことができます。

aliastest6.sh

#!/usr/bin/bash
shopt -s expand_aliases
alias main_alias="echo main alias"

source ./aliastest5_sub.sh
echo "---- main()内で定義されているalias一覧----------"
alias
echo "------------------------------------------------"
$ ./aliastest6.sh
---- main()内で定義されているalias一覧----------
alias main_alias='echo main alias'
alias sub_alias='echo sub alias'
------------------------------------------------

bashでラッパー関数の引数をうまく処理する

シェルスクリプトで何かのコマンドを実行したい場合、そのコマンドの実行前後に他の処理を入れたり、コマンドに特定のオプションをつけたりしたいことが出てくることがあります。他にも、メインの関数の前に他の関数を挟んだりしたいこともあります。この場合、本来実行したいコマンドや関数のラッパー関数を作ることになります。

ラッパー関数を作りたい場合その関数の引数全部を「$@」という特別な変数を参照できますが、この変数は配列です。そのため「$@」を他の変数にそのまま代入したい場合、そのまま参照すると想定外の挙動になるので、以下のように代入しましょう。

ARGS=("$@")

上記で代入するとARGSは配列になるので、各引数も以下のように取り出せます。

ARG1=${ARGS[0]}
ARG1=${ARGS[1]}

ラッパー関数なので、本来実行したい関数やコマンドにそのまま引数で渡したい場合も、配列として全要素を渡しましょう。

myfunc.sh

#!/bin/bash

function main {
    echo "---- main ----"
    local ARGS=("$@")
    echo "arg1=${ARGS[0]}"
    echo "arg2=${ARGS[1]}"
    echo "arg3=${ARGS[2]}"
    echo "arg4=${ARGS[3]}"
}

function myfunc {
    echo "---- myfunc ----"
    local ARGS=("$@")
    echo "arg1=${ARGS[0]}"
    echo "arg2=${ARGS[1]}"
    echo "arg3=${ARGS[2]}"
    echo "arg4=${ARGS[3]}"

    main "${ARGS[@]}"
}

myfunc "a b" c "d" "e f g"
$ ./myfunc.sh
---- myfunc ----
arg1=a b
arg2=c
arg3=d
arg4=e f g
---- main ----
arg1=a b
arg2=c
arg3=d
arg4=e f g
やりがちなミス

ARGS代入時に$@をダブルクォーテーションで囲み忘れると、引数にスペースが入っている場合に想定外の動作をするのでご注意ください。

myfunc.sh

#!/bin/bash
function myfunc {
    local ARGS=($@)
    echo "arg1=${ARGS[0]}"
    echo "arg2=${ARGS[1]}"
    echo "arg3=${ARGS[2]}"
    echo "arg4=${ARGS[3]}"
    echo "arg5=${ARGS[4]}"
    echo "arg6=${ARGS[5]}"
    echo "arg7=${ARGS[6]}"
}

myfunc "a b" c "d" "e f g"
$ ./myfunc.sh
arg1=a
arg2=b
arg3=c
arg4=d
arg5=e
arg6=f
arg7=g

逆にARGSに配列として代入しないと、以下のように引数全体が一つの文字列になってしまいます。

myfunc.sh

#!/bin/bash
function myfunc {
    local ARGS="$@"
    echo "args=${ARGS}"
    echo "arg1=${ARGS[0]}"
    echo "arg2=${ARGS[1]}"
}

myfunc "a b" c "d" "e f g"
$ ./myfunc.sh
args=a b c d e f g
arg1=a b c d e f g
arg2=

curlで取得したスクリプトに引数

インストール用シェルスクリプトをcurlでダウンロードし、そのままシェルに渡して実行することが増えてきました。

例えば、HELM をインストールする場合、以下のコマンドでインストールする方法が紹介されています。

$ curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

上記スクリプトを実行すれば、最新版のHELMがインストールされますが、実は --version という引数を与えることで、バージョンを指定することができます。これを、上記のようにcurlでダウンロードして直接bashで実行したい場合、bash の -s オプションを使えばできます。

試しに、curl でダウンロードしたと想定して、「echo $1 $2」を実行するスクリプトに、123 と 456 を引数で与えると、このように引数の内容が表示されます。

$ echo 'echo $1 $2' | bash -s 123 456
123 456

なお、引数が "--version" のように "-" があると、bash の引数と理解されてしまうので、その場合は、"--" を -s の後ろにつけましょう。

$ echo 'echo $1 $2 $3' | bash -s -- --version 123
--version 123

find コマンドで全てのファイルにマッチしない!

Linuxでファイル検索する場合の基本コマンドに、「find」があります。ファイル名に含まれる文字列の一部を検索したりできるのですが、検索条件をちょっと間違えるだけで、目的のファイルが見つからなく(マッチしなく)なるので、注意が必要です。

例えば、以下の構成のディレクトリとファイルがあるとします。

# tree -F root
root/
└── my-dir/
    ├── dir1/
    │   └── memo2.txt
    ├── dir2/
    │   └── memo2.txt
    └── memo.txt

この時、拡張子が .txt のファイルを探したいとします。root というディレクトリ内で以下のfindを実行してみます。

# cd root/
# find . -name *txt
./my-dir/dir2/memo2.txt
./my-dir/memo.txt
./my-dir/dir1/memo2.txt

目的の .txt 拡張子のファイルが全てマッチしました。

では、my-dir ディレクトリの中で同じコマンドを実行するとどうなるでしょうか?

# cd my-dir/
# find . -name *txt
./memo.txt

このように、memo.txt しかマッチしなくなりました。なぜ、このような事象が起きてしまうのでしょうか? 実は、今回このコマンドを実行しているシェル(bash)の仕様が関係しているのです。

bash でのアスタリスク 「* 」はワイルドカード展開を意味します。つまり、カレントディレクトリに *txt にマッチするファイルがあると、コマンド実行前にそのファイル名に置き換えられるわけです。実際にコマンドを叩いて試してみます。

上記ディレクトリ構成で、root ディレクトリ内で、echo *txt を実行してみます。

# cd root
# echo *txt
*txt

my-dir ディレクトリでも同様にechoを実行してみます。

# cd my-dir/
# echo *txt
memo.txt

このように、root ディレクトリ内には、*txt にマッチするファイルがないので、アスタリスクが置き換わらず、echo 出力に *txt と表示されましたが、my-dir ディレクトリ内には、memo.txt というファイルがあるので、echo出力は、memo.txt に置き換わってしまいました。

つまり、my-dir 内でfindを実行した場合、

# find . -name *txt

# find . -name memo.txt

を実行していることになり、結果、memo.txt にしかマッチしないという悲劇を生み出すわけです。ここまで理解できれば、対処方法は簡単です。シェルにアスタリクスを処理させなければよいわけです。例えば、アスタリクスをエスケープ(\)すると、マッチするようになります。

# find . -name \*txt
./dir2/memo2.txt
./memo.txt
./dir1/memo2.txt

ただ、この方法は面倒だと思うので、シングルクォートで囲む方が一般的だと思います。

# find . -name '*txt'
./dir2/memo2.txt
./memo.txt
./dir1/memo2.txt

以上findのちょっとしたtipsでした。

find でファイル名の文字列検索時は、
必ずシングルクォートで囲みましょう!

標準出力にタイムスタンプを付与する@Linux

シェルスクリプトの実行結果をファイルに保存する時など、タイムスタンプが欲しい時があります。いくつか方法はあると思いますが、ここではLinuxに標準でインストールされてるawk使って簡単に実現してみます。

$ ping 172.16.0.1 | awk '{print strftime("%F %T ") $0; fflush();}'
2017-07-02 21:34:29 PING 172.16.0.1 (172.16.0.1) 56(84) bytes of data.
2017-07-02 21:34:29 64 bytes from 172.16.0.1: icmp_seq=1 ttl=64 time=2.44 ms
2017-07-02 21:34:30 64 bytes from 172.16.0.1: icmp_seq=2 ttl=64 time=2.01 ms
2017-07-02 21:34:31 64 bytes from 172.16.0.1: icmp_seq=3 ttl=64 time=1.95 ms
2017-07-02 21:34:32 64 bytes from 172.16.0.1: icmp_seq=4 ttl=64 time=2.09 ms

 

簡単ですね。スクリプト内で毎回awkを呼び出すのは面倒な場合は、coproc コマンドやexecを組み合わせてみましょう。

#!/bin/bash

TTY=`tty`
coproc awk '{print strftime("%F %T ") $0; fflush();}' > $TTY
exec >&${COPROC[1]} 2>&1

ping 172.16.0.1

 

coproc で awk コマンドをバックグラウンドで起動し、awkの標準出力を自分のttyにリダイレクトします。さらに、exec スクリプト内の全標準出力・標準エラー出力をcoprocで起動したawkの標準入力にリダイレクトしています。

結果、ping の出力結果が全てがawk経由で自分のターミナル上に出力されます。

シェルスクリプト内のコマンドのエラー出力をファイルに一括リダイレクト

通常、シェルスクリプトのエラー出力をファイルに保存したい場合、以下のように、「コマンド 2>ファイル名」 でファイルに追記でリダイレクトすることがあります。

ファイル名 : myscript.sh

#!/bin/bash

ls /hoge 2>> err.log
cat /hoge 2>> err.log

上記スクリプトを実行し、err.logを確認します。

$ ./myscript.sh
$ cat err.log
ls: '/hoge' にアクセスできません: No such file or directory
cat: /hoge: No such file or directory

ただし、シェルスクリプトが長くなってくると、全てのコマンドにリダイレクトを記述するのは面倒です。そういった場合は、exec を使って一括でリダイレクトすることが可能です。

#!/bin/bash

exec 2> err2.log
ls /hoge
cat /hoge

$ ./myscript.sh
$ cat err2.log
ls: '/hoge' にアクセスできません: No such file or directory
cat: /hoge: No such file or directory

Windows bashを使ってみた

Windows10 の Anniversary Updateで、Ubuntu が使えるようになったとのことなので、早速使ってみた。

インストール方法は、いろいろと記事があるので割愛。早速中身を漁ってみる。

コマンドプロンプトで "bash" を起動

C:\>bash
user@PC:/mnt/c$

 

どうやらbashを起動すると、コマンドプロンプトのカレントフォルダが、bashのカレントフォルダ(Linuxなのでカレントディレクトリ??) になるようだ。ユーザはbashインストール時に作ったユーザ(ここではuserというユーザ)で、sudo権限を持ってます。

/mnt に、Windowsのドライブが割り当てられており、/mnt/c がCドライブで、ユーザのホームディレクトリは普通のLinux同様、/home/配下になっていた。

user@PC:~$ echo $HOME
/home/user

 

Ubuntuのディストリビューションを確認

user@PC:~$ cat /etc/issue
Ubuntu 14.04.4 LTS \n \l

14.04 LTS が入っていた。

Linux上では必ず使う、screenコマンドを叩いてみる。

user@PC:~$ screen
[screen is terminating]

 

screenコマンドは動かないようだ。ネットを少し調べてみたが解決方法もわからず、一旦諦めることに。ちなみにtmuxコマンドは動いた。私はscreen派なのだが、これを期にtmuxに乗り換えるのもありかと考え中。

次に、Ubuntuのパッケージ管理コマンド apt-get を叩いてみる。

user@PC:~$ apt-get update
E: ロックファイル /var/lib/apt/lists/lock をオープンできません - open (13: 許可がありません)
E: ディレクトリ /var/lib/apt/lists/ をロックできません
E: ロックファイル /var/lib/dpkg/lock をオープンできません - open (13: 許可がありません)
E: 管理用ディレクトリ (/var/lib/dpkg/) をロックできません。root 権限で実行していますか?

 

一般ユーザなので、apt-get が叩けなかった・・・ 気を取り直してrootユーザでapt-get

user@PC:~$ sudo apt-get update

応答が返ってこない。  (追記 : 実際は応答が返ってこないのではなく、非常に応答時間がかかっていることが後で判明)

user@PC:~$ sudo su -

というか、そもそもsudoの応答が返ってこない・・  (追記 : これも応答に時間がかかっているだけであることが後で判明)

Windowsのコマンドプロンプトが管理者権限で動いてないからかな?と思い、コマンドプロンプトを右クリックして、管理者として実行し、bashを起動して、リトライ。

user@PC:~$ sudo su -
root@PC:~# apt-get update
無視 http://archive.ubuntu.com trusty InRelease
ヒット http://archive.ubuntu.com trusty-updates InRelease
ヒット http://archive.ubuntu.com trusty Release.gpg
ヒット http://archive.ubuntu.com trusty-updates/main amd64 Packages
~ 略 ~

動いた! root権限が必要な場合は、コマンドプロンプトも管理者として実行しておけばスムーズに作業ができそうなのがわかった。

試しに、unzipコマンドを入れてみる。

root@PC:~# apt-get install unzip
パッケージリストを読み込んでいます... 完了%
依存関係ツリーを作成しています
状態情報を読み取っています... 完了%
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
 libfreetype6 os-prober
これを削除するには 'apt-get autoremove' を利用してください。
提案パッケージ:
 zip
以下のパッケージが新たにインストールされます:
 unzip
アップグレード: 0 個、新規インストール: 1 個、削除: 0 個、保留: 48 個。
157 kB のアーカイブを取得する必要があります。
この操作後に追加で 395 kB のディスク容量が消費されます。
警告: 以下のパッケージは認証されていません!
 unzip
検証なしにこれらのパッケージをインストールしますか? [y/N] y
取得:1 http://archive.ubuntu.com/ubuntu/ trusty-updates/main unzip amd64 6.0-9ubuntu1.5 [157 kB]
157 kB を 3秒 で取得しました (50.1 kB/s)
以前に未選択のパッケージ unzip を選択しています。
(データベースを読み込んでいます ... 現在 25209 個のファイルとディレクトリがインストールされています。)
.../unzip_6.0-9ubuntu1.5_amd64.deb を展開する準備をしています ...
unzip (6.0-9ubuntu1.5) を展開しています...
man-db (2.6.7.1-1ubuntu1) のトリガを処理しています ...
mime-support (3.54ubuntu1.1) のトリガを処理しています ...
unzip (6.0-9ubuntu1.5) を設定しています ...

問題なさそう。

/proc ファイルシステムを覗いてみたが、ちゃんと見れた。

root@PC:~# cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 78
model name : Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
stepping : 3
microcode : 0xffffffff
cpu MHz : 2400.000
cache size : 256 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 2
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 6
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse
sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr pdcm
pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave osxsave avx f16c rdrand
bogomips : 4800.00
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management:
~ 略 ~

 

mountコマンド。

root@PC:~# mount
rootfs on / type rootfs (rw,relatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=204320k,mode=755)
none on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
none on /run/shm type tmpfs (rw,nosuid,nodev,relatime)
none on /run/user type tmpfs (rw,nosuid,nodev,noexec,relatime,size=102400k,mode=755)

 

今日はこのあたりまで。

gitのbash-completionを使用してのプロンプト変更@Ubuntu 14.04

Git 最低限の設定@CentOS 5 でGitのbash_completionを使っていましたが、同じような設定をUbuntu 14.04 LTS上で設定したときのメモです。

結論から言うと、あまりやることありません。 bash-completion とgitをインストールするだけで、ログイン時に自動で設定は読み込まれるようです。(呼び出し元はここかな /etc/profile.d/bash_completion.sh)

# apt-get install bash-completion git

あとは、前回と同じように、プロンプトを書き換えるPS1をこのように設定するだけです。

# export PS1='\u@\h:\w$(__git_ps1 " (%s)")\$ '

毎回設定するのが面倒なら、~/.bashrcファイルに追記しておけばOKです。

 

なお、screenコマンドを使うと、bash-completionの設定が読み込まれず、コマンド実行する度に、以下のエラーが出力されます。

-bash: __git_ps1: コマンドが見つかりません

その場合は、~/.screenrc ファイルに以下の設定を書いてログインシェルと同じにすることで解決します。

defshell -bash

設定変更後はscreenコマンドに再度入り直す必要があります。

 

前回の記事と同様に、screenコマンド使用時にスクリーン番号表示するには、~/.bashrcファイルに以下のように記述します。

if [ "$TERM" == "screen" ]; then
    export PS1='\h:$WINDOW:\w$(__git_ps1 " (%s)")\$ ' 
else
    export PS1='\h:\w$(__git_ps1 " (%s)")\$ ' 
fi

screen実行時のプロンプトに、このようにスクリーン番号とgitのブランチ名が出力されるようになります。

root@ubuntu:0:/home/repository/test-repo (master)#

シェルで標準出力と標準エラーをそれぞれ違う異なるコマンドにパイプで渡す@bash

あるコマンドの標準出力と標準エラー出力をそれぞれ別々のコマンドで処理するには、以下のようにすればOKです。

(command | normalcmd) 2>&1 | errorcmd

上記では、commandの標準出力をnormalcmdへ、標準エラー出力をerrorcmdへ渡しています。

シェルで2進数、8進数、10進数、16進数変換@bash

シェルスクリプトで2進数や16進数を扱う方法です。bc コマンドを使う方法と、printf コマンドを使う方法、シェル(bash)の機能を使う方法の3つを紹介します。

bcコマンドを使う方法

16進数"A"を10進数に変換。
※Aは大文字にする必要があります。

# echo "obase=10; ibase=16; A" | bc
10

obase=10が出力形式(=10進数)、ibase=16が入力の形式(=16進数)です。デフォルトは10なので、obaseは省略することができます。なお、obase → ibaseの順で書く必要があり、逆にすると以下のように動きませんでした。

# echo "ibase=16; obase=10; A" | bc
A

16進数を2進数に変換

# echo "obase=2; ibase=16; F" | bc
1111

こちらはobaseとibaseを入れ替えても以下の通り動きましたが、バグを出さないためにも、obase → ibase の順で書くのがよさそうです。

# echo "ibase=16; obase=2; F" | bc
1111

printfコマンドを使う方法

16進数"A"を10進数に変換

# printf '%d\n' 0xA
10

0xを付けて"A"が16進であることを表してます。シングルクォートの中が出力フォーマットで、%d が10進数で出力することを表します。Cのprintfと同じで改行されないので、\n を追加しています。

 

10進数"10"を16進数に変換。

# printf '%x\n' 10
a

# printf '%X\n' 10
A

%xが16進数を意味し、xが小文字の場合16進数のアルファベット部分は小文字で出てき、大文字にするとアウトプットも大文字になります。

 

10進数の8を8進数に変換。

# printf '%o\n' 8
10

 

8進数を入力する場合は、頭に0(ゼロ)をつけます。

# printf '%d\n' 010
8

なお、printfで2進数は扱えないようです。

シェルの機能を使う方法

シェル(bash)の機能でも変換できます。
以下は16進数(F) を10進数に変換する例です。

# echo $((0xF))
15

8進数(10)を10進数に変換

# echo $((010))
8

任意の基底(xx進数)を選ぶ事もできます。例えば、頭に2#を付けると2進数を意味します。2進数11 を10進数に変換する例です。

# echo $((2#11))
3

なお、10進数以外には変換できなさそうです。

標準出力と標準エラー出力を入れ替える方法@bash

標準出力と標準エラー出力を入れ替える方法を、結論だけを先に書くと

3>&2 2>&1 1>&3

というリダイレクトを行うと可能です。

 

以下は、あまりリダイレクトに詳しくない人向けに説明書いてみました。

ファイルディスクリプタは、0番が標準入力、1番が標準出力、2番が標準エラー出力です。例えば、index.html というファイルがあり、abc.htmlが無いディレクトリで以下のコマンドを実行したとすると、以下のように表示されます。

# ls -l index.html abc.html
ls: cannot access abc.html: そのようなファイルやディレクトリはありません
-rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

このとき、見た目はわかりませんが、結果1行目の"ls: cannot access abc.html: そのようなファイルやディレクトリはありません" というのが標準エラー出力、2行目の"-rw-r--r-- 1 root root 0  12月 30 15:15 2012 index.html" が標準出力になります。

この二つの違いを知るには、単純に標準出力をリダイレクトしてみるとわかります。

# ls -l index.html abc.html > log.txt
ls: cannot access abc.html: そのようなファイルやディレクトリはありません

このように、">" は標準出力だけをリダイレクトするため、標準出力だけがlog.txtに書き込まれ、標準エラー出力はそのまま画面に出力されます。

# ls -l index.html abc.html 2> log.txt
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

 

次は、パイプを使って結果を処理することを考えたいと思います。今回は、よく使われるgrepを使います。

# ls -l index.html abc.html | grep index
 ls: cannot access abc.html: そのようなファイルやディレクトリはありません
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

はい。知っている人には当たり前ですが、"index"という文字列をgrepしたのに、エラーの内容も表示されました。これは、パイプが標準出力だけをgrepの入力として処理し、標準エラー出力はパイプに渡されずにそのまま画面に出力されてるためです。

標準エラー出力もgrepで処理したい場合は、以下のように標準エラー出力を標準出力にリダイレクトしてあげることで解決します。

# ls -l index.html abc.html 2>&1 | grep index
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

 

では標準エラー出力だけをgrepし、標準出力はそのまま画面に出力させたい場合はどうすればよいのでしょうか?

言い換えれば、標準出力と標準エラー出力を入れ替えるにはどうすればよいでしょうか?

 

bashには、リダイレクトを入れ替えるという機能はありません。そこで登場してもらうのが、3番のファイルディスクリプタです。考え方は、標準出力(1番)を一時的に3番に退避し、標準エラー出力(2番)を1番にリダイレクト、退避していた3番を2番にリダイレクトするという方法です。これをコマンドで書くとこのようになります。 ※説明の順番を以下のようにコマンドの後ろから書いていく必要があります。

# ls -l index.html abc.html 3>&2 2>&1 1>&3
 ls: cannot access abc.html: そのようなファイルやディレクトリはありません
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

見た目では分かりませんが、これで入れ替わりました。先ほどと同じようにgrepしてみましょう。

# ls -l index.html abc.html 3>&2 2>&1 1>&3 | grep index
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

はい。最初のgrepの例と異なり、"ls: cannot access abc.html: そのようなファイルやディレクトリはありません"だけがgrepの対象になり、、"-rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html" は標準エラー出力として出力されているためそのまま画面に出力されました。

この例だと、grepでマッチしているように見えるので、違う文字列をgrepしてみます。

# ls -l index.html abc.html 3>&2 2>&1 1>&3 | grep def
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html
# ls -l index.html abc.html 3>&2 2>&1 1>&3 | grep abc
 ls: cannot access abc.html: そのようなファイルやディレクトリはありません
 -rw-r--r-- 1 root root 0  4月 30 15:54 2012 index.html

ちゃんと入れ替わってるでしょ?

ドメイン名一覧をドメイン毎にソート(文字列の反転)@bash

Webページのアクセス解析をドメイン毎に集計してみようと思って、IPを抜き出しホスト名を逆引き、そしてsortしようとしたが、sortコマンドは単純に文字列を並べるだけなので、ドメイン毎に集計できません。

例えば、aaa.com でひとまとめにしたくても、ホスト名が似た別ドメインがあると、このようにソートされてしまいます。

111.aaaa.com
222.bbbb.com
333.aaaa.com

ではどうやってやればよいかと、文字列を後ろからソートすればよいのですが、sort コマンドにはそのような機能はありません。

そこで出てくるのが、rev コマンドです。 このコマンドの動きは単純で、入力された文字列を反転、つまりこのように逆順に並び替えてくれます。

# echo  12345 | rev
 54321

よって、以下のように反転->ソート->反転 とすることで、ドメイン毎に集計ができるようになります。

# cat iplist.txt | rev | sort | rev
111.aaaa.com
333.aaaa.com
222.bbbb.com

printfコマンドで数字を整形表示@Linux

echoコマンドはシステムによって挙動が異なる、特に改行関連を扱うオプションに差異があるので、悩みたくない場合はprintfコマンドを使うべし。

# printf "hoge\n"

あと、printfだと表示を整形できるからちょっと便利。Cのprintfに似てます。

数値は %d で置き換え

# printf "[%d]\n" 30
[30]

5桁で右詰め

# printf "[%5d]\n" 30
[   30]

5桁で左詰め

# printf "[%-5d]\n" 30
[30   ]

5桁でゼロパディング

# printf "[%05d]\n" 30
[00030]

パイプでつないだコマンドの戻り値を調べる@bash

シェルスクリプトで、実行したコマンドの戻り値を知る方法は有名ですよね。

以下のように、$? で知ることができます。 ※false は つねに 1 を返すコマンドです。

# false; echo $?
1

しかし、パイプ "|" でつないだ場合、$? で知ることができるのは、最後のコマンドの結果だけです。

# true | true | false; echo $?
1

1番目や2番目の結果を知りたい場合どうすればよいかというと、PIPESTATUS とう配列をみてあげるとわかります。

例えば、上記のコマンド結果を以下のように表示してあげると、PIPESTATUS[0] には、最初のtrueの結果が、PIPESTATUS[1]には、2番目のtrueの結果が、PIPESTATUS[2]には3番目のfalseの結果がわかります。

# true | true | false; echo ${PIPESTATUS[0]}
0
# true | true | false; echo ${PIPESTATUS[1]}
0
# true | true | false; echo ${PIPESTATUS[2]}
1

ハイフンで始まるファイルを消す@Linux

ハイフンで始まる以下のようなファイル(-hoge)を作ったとします。

# echo > -hoge

このファイルを消そうとすると、エラーになって消せません。

# rm -hoge
rm: 無効なオプション -- 'h'
Try `rm ./-hoge' to remove the file `-hoge'.
詳しくは `rm --help' を実行して下さい.

これは、-hoge がrmコマンドのオプションとして解釈されてしlまうのが原因です。こういうときは "--" を使って、この先オプションではありませんよと教えてあげれば消すことができます。

# rm -- -hoge
rm: remove 通常ファイル `-hoge'? y

このz「--」は、mvやcp, ls といった標準的なコマンドではどれも共通のようです。

コマンドラインで画像編集@Cygwin

シェルスクリプトで画像を扱いたい場合は、 ImageMagick というソフトが便利。Linuxのディストリビューションには付属しているものもある (convertコマンド) が、CygwinにはないのでこれをインストールすればOK。

どんなことができるか簡単に紹介します。

画像フォーマット変換 (abc.gif をdef.jpgに変換)

# convert abc.gif def.jpg

画像サイズ変更

# convert abc.gif -resize 150% output.gif

画像結合 (上の画像 abc.jpg , 下の画像 def.jpg)

# convert append abc.jpg edf.jpg  output.jpg

文字(hoge)を入れることだってできちゃいます。

convert -pointsize 30 -draw "text 0,30 hoge"  abc.jpg def.jpg

0,30は、文字列の左下の座標です。0,0にすると文字が見えなくなるので注意。

このブログについて
プライバシーポリシー・お問い合わせ等
購読する(RSS)
記事検索
アーカイブ
カテゴリー
  • ライブドアブログ