0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Shellの基本を学ぶ(8)String の操作

Posted at

Parameter Expansion

  • ${#var}: $var の長さ

String の一部を取り除く

  • ${var#pattern} 文字列の初めからもっともショートマッチした部分を削除
  • ${var##pattern} 文字列の初めから、もっとロングマッチした部分を削除
  • ${var%pattern} 文字列の最後からもっともショートマッチした部分を削除
  • ${var%%pattern} 文字列の最後からもっともロングマッチした部分を削除

*, ?, [] などもブラケットの中で使用可能。

Example i="/Users/Tsuyoshi/demo.txt"

${i#*/} Users/Tsuyoshi/demo.txt
${i##*/} demo.txt
${i%.*} /Users/Tsuyoshi/demo
${i%/*} /Users/Tsuyoshi

Search と Replace

  • ${var/pattern/string} 最初のマッチを置き換える
  • ${var//pattern/string} 全てのマッチを置き換える
  • ${var/#pattern/string} 最初の文字を置き換える
  • ${var/%pattern/string} 最後からマッチしたものを置き換える

demo

$i="myjpg.jpg"
$echo ${i/jpg/txt}
mytxt.jpg
$echo ${i/%jpg/txt}
myjpg.txt
$echo ${i//jpg/txt}
mytxt.txt
$echo ${i//jpg}
my.
$echo ${i/%jpg/}
myjpg.
$echo ${i#jpg}
myjpg.jpg  # String がjpg で始まっていないため。
$echo ${i/[yj]/a}
majpg.jpg
$echo ${i//[yj]/a}
maapg.apg

mvext

# !/bin/bash

[[ $# -ne 2 ]] && { echo "Missing two arguments" >&2; exit 1; } 

for f in *"$1"; do
  mv "$f" "${f/%$1/$2}"
done

実行

$ls
a.jpg	b.jpg	c.jpg
$mvext jpg txt
$ls
a.txt	b.txt	c.txt

Default value

${var:-value}
${var-value} var 空のケースに限る (上記の省略系)
${var:=value} varがカラだったり、セットされていないケースにアサインする
${var=value} var がunsetの時のみ

サンプル

次の例は$NOTESDIR がある場合は、その値、なければ $HOMEが使われる。

declare notesdir=${NOTESDIR:-$HOME}

コンディショナル

Pattern matching

  • [[ .. ]] の中の == != はパターンマッチングを行う
  • [[ $var == pattern ]]$var がパターンにマッチすると true を返す
  • パターンマッチは、*, ? , [] などを用いる
  • ダブルクオートで囲むと、パターンマッチではなく、文字列として比較を行う

サンプル

パターンマッチが上記の通り行われている。ダブルクオートだと、パターンマッチではなく、文字列そのものとしてマッチする。最後のものは、間違えて[ .. ] にしたもの。この場合、t*i は、ファイル名として解釈される。

$[[ tsuyoshi = t*i ]] && echo yep
yep
$[[ tsuyoshi = "t*i" ]] && echo yep
$[[ "t*i" = "t*i" ]] && echo yep
yep
$[ tsuyoshi = t*i ] && yep
$ls
a.txt		box		count		d.txt		hw		read_pipe
b.txt		c.txt		create_script	hist		mvext		test
$echo [ m*t ]
[ mvext ]
$

Regular Expression

正規表現

  • =~正規表現のマッチ

?

? はトークンが0もしくは1回の出現にマッチする。[0-9]? は1文字の数字もしくは、何もない変数にマッチする。

*

* は全てのトークンにマッチする。[a-z]* は全ての小文字の英字もしくは、何もないのにマッチする。

+

+は1回以上にマッチする。[0-9]+は、1回以上の数字が出て来ることにマッチする

^

文字の開始にマッチする

$

文字の終了にマッチする

.

任意の一文字

サンプル

以前のカウントを改造する。isnumメソッドを改造してパターンマッチを使う。ポイントとしては、最初に、変数に正規表現を代入して使っている。シングルクオートを使っている。これは、エスケープをするのが面倒なので、シングルクオートを使って、変数に代入することでそれを避けられるから、コードが楽になる。

^[0-9]+$というのは、先頭文字^ から、数字[0-9]何文字か+最後まで$ という意味なので、任意の数字にヒットする。^0(.+)というのは、先頭^が0その後続は任意の文字(複数)(.+) という意味。つまり先頭が0でそのあとは任意。数字チェックをしたif の中にいるので、当然数字で0始まり、つまり、8進数表現を表している。(Bashの場合)。

ちなみに、$BASH_REMATCH は正規表現にマッチした内容が配列になって入っているもの。$BASH_REMATCH[0] は全体で、$BASH_REMATCH[1] 以降はマッチの部分が入っていく。この場合は、0を除いた数字が入る。

isnum() {
    declare -r num_re='^[0-9]+$'
    declare -r octal_re='^0(.+)'
    num_error="ok"
    if [[ $1 =~ $num_re ]]; then
      if [[ $1 =~ $octal_re ]]; then
        num_error="$1 is not a number, did you mean ${BASH_REMATCH[1]}?"
        return 1
      fi
    else 
      num_error="$1 is not a number"
      return 1
    fi

    return 0
}

実行

実行するとちゃんと8進数が弾かれる。0も、^0(.+)+が一文字以上なので、マッチしないので問題ない。

$count 020
Error: 020 is not a number, did you mean 20?
count [-r] [-b n] [-s n] stop

Print each number up to stop, beginning at 0
   -b: number to begin with (default: 0)
   -h: show this help message
   -r: reverses the count
   -s: sets step size (default: 1)
$count 0
0

全てのコード

count

# !/bin/bash

usage () {
    cat <<END
count [-r] [-b n] [-s n] stop

Print each number up to stop, beginning at 0
   -b: number to begin with (default: 0)
   -h: show this help message
   -r: reverses the count
   -s: sets step size (default: 1)
END
}

error() {
    echo "Error: $1"
    usage 
    exit $2
} >&2

isnum() {
    declare -r num_re='^[0-9]+$'
    declare -r octal_re='^0(.+)'
    num_error="ok"
    if [[ $1 =~ $num_re ]]; then
      if [[ $1 =~ $octal_re ]]; then
        num_error="$1 is not a number, did you mean ${BASH_REMATCH[1]}?"
        return 1
      fi
    else 
      num_error="$1 is not a number"
      return 1
    fi

    return 0
}

declare reverse=""
declare -i start=0
declare -i step=1



while getopts ":hb:s:r" opt; do 
echo "OPTARG: ${OPTARG}"
  case $opt in
    r)
      reverse="yes"
      ;;
    h)
      usage 
      exit 0
      ;;
    b)
      isnum ${OPTARG} || error "${num_error}" 1
      start="${OPTARG}"
      ;;
    s)
      isnum ${OPTARG} || error "${num_error}" 1
      step="${OPTARG}"
      ;;
    :)
      echo "Option -${OPTARG} is missing an argument"
      exit 1
      ;;
    \?)
      echo "Unknown option: -${OPTARG}" >&2
      exit 1
      ;;
  esac
done

shift $(( OPTIND -1 ))

[[ $1 ]] || { echo "Argument is missing" >&2; exit 1; }
declare end="$1"

isnum $1 || { error "${num_error}" 1 ; exit 1; }

if [[ ! $reverse ]]; then
  for (( i=start; i <= end; i+=step )); do
    echo $i
  done
else
  for (( i=end; i >= start; i-+step )); do
    echo $if
  done
fi

exit 0

End of options

-- この後の引数は、評価されない。例えば-l.txt というファイルがあるとすると、引数と勘違いされる。rm -- -l.txt とやるとうまくいく。そういったファイル名を含む場合に、ファイル操作したい場合、

$ for in *.txt; do touch -- $i; done

と言った書き方もできる。引数としての変数にを使うときに、使うのは良い習慣。引数は、自分でコントロールできないので、コマンドを呼ぶときに、-- を使う

サンプル

-a.txt というファイル名は通常作れませんが、--を使うと、可能になります。

$touch -a.txt
touch: illegal option -- .
usage:
touch [-A [-][[hh]mm]SS] [-acfhm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...
$touch -- -a.txt
$ls
-a.txt	a.txt	b.txt	c.txt

既存のプログラムmvextを動作させてみますが、このようなファイル名が含まれている場合は動きません。

$mvext txt jpg
mv: illegal option -- a
usage: mv [-f | -i | -n] [-v] source target
       mv [-f | -i | -n] [-v] source ... directory

出来るように改造してみます。mv の所に --をつけて、パラメータと思われないようにします。

mvext

# !/bin/bash

[[ $# -ne 2 ]] && { echo "Missing two arguments" >&2; exit 1; } 

for f in *"$1"; do
  mv -- "$f" "${f/%$1/$2}"
done

すると

$mvext txt jpg
$ls
-a.jpg	a.jpg	b.jpg	c.jpg

綺麗に動作します。他にも、パラメータではない部分には、入れといたほうが良さそうです。

tn

# !/bin/bash

declare -r date=$(date)

declare -r topic="$1"

declare notesdir=${NOTESDIR:-$HOME}

if [[ ! -d $notesdir ]]; then
  mkdir -- "${notesdir}" 2> /dev/null || { echo "Cannot make directory ${notesdir}" 1>&2 ; exit 1; }
fi

declare -f filename="${notesdir}/${topic}notes.txt"
if [[ ! -f $filename ]]; then
  touch -- "${filename}" 2> /dev/null || { echo "Cannot create file ${filename}." 1>&2 ; exit 1;}
fi

[[ -w $filename ]] || { echo "${filename} is not writable" 1>&2; exit 1; }

read -p "Your note: " note

if [[ $note ]]; then
  echo "$date: $note" >> "$filename"
  echo "Note '$note' saved to $filename" 1>&2
else
  echo "No input; note wasn't saved." 1>&2
  exit 2
fi

exit 0
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?