Linuxシェルスクリプトで文字列の部分取得

Bashで文字列の一部を取得する基本形は${text:offset:length}です。offsetは0から始まり、lengthを省略すると末尾までを返します。たとえば${text:0:5}は先頭5文字です。ただし、負のoffsetは既定値展開:-と区別する空白が必要で、外部入力をoffsetへそのまま入れるとBashの算術式として再評価されます。日本語や絵文字では文字数、UTF-8バイト数、画面上の1文字が一致しない場合もあります。目的と単位を先に決めてください。

目次

先頭から固定文字数を取得する

ASCIIの識別子から先頭4文字を取る例です。結果は新しい変数へ入れ、ダブルクォートで表示します。元の変数は変更されません。offsetが文字列長以上なら空文字になりますが、用途によっては入力エラーとして事前に長さを検証します。

text='abcdef12345'
prefix=${text:0:4}
printf 'prefix=%s\n' "$prefix"

波括弧内のコロンは部分文字列演算子です。${text:-default}は未設定・空の既定値という別構文で、offsetを省略した意味ではありません。読みやすさのためoffsetとlengthを説明的な変数へ置くこともできます。

途中から末尾まで

${text:6}は0始まりの位置6から末尾までを返します。位置はバイトオフセットではなく、Bashと現在のロケールが認識する文字単位として扱われるのが一般的です。区切り文字の位置を固定値で仮定すると、前半の長さが変わったときに壊れます。構造化されたIDなら形式を検証し、区切り基準の処理を使います。

text='order-20260716'
suffix=${text:6}
printf 'suffix=%s\n' "$suffix"

末尾から取得する負のoffset

末尾4文字を取りたい場合は、負のoffsetを使えます。Bashが:-の既定値構文と混同しないよう、コロンの後に空白を置く形が重要です。${text: -4}と書きます。短い入力では全体が返るなど境界挙動を確認し、厳密に4文字必要なら長さを先に検証します。

text='report.txt'
last_four=${text: -4}
printf 'last=%s\n' "$last_four"

拡張子取得に「末尾4文字」を使うと、.jpeg、.tar.gz、拡張子なし、先頭ドットの設定ファイルで失敗します。パスの構造を扱うならbasenameなどの仕様、ファイル形式判定ならfileやMIME検査を使い、固定文字数だけでセキュリティ判断をしません。

負のlengthの意味

Bashでは文字列に対する負のlengthが、offsetから末尾より手前の位置までを返す形として使える版があります。配列や位置パラメーターでは負のlengthがエラーになるなど対象により意味が異なります。移植性と可読性が必要なら、文字列全長から終了位置を明示計算し、検証した非負の長さを使います。対象のBash版でテストしてください。

text='abcdefghij'
start=2
end=8
length=$(( end - start ))
part=${text:start:length}
printf 'part=%s\n' "$part"

offsetとlengthを外部入力から受け取る

Bashの部分文字列に使うoffsetとlengthは算術式として解釈されます。外部入力に変数名や演算子が入ると、意図しない追加評価が起こり得ます。コマンドラインや設定ファイルから受け取る場合は、まず10進の非負整数だけを許可し、文字列長との範囲を確認します。符号、空文字、先頭0、非常に大きい値の扱いも決めます。

offset=$1
length=$2
case $offset in
  ''|*[!0-9]*) printf '%s\n' 'offset must be a non-negative integer' >&2; exit 2 ;;
esac
case $length in
  ''|*[!0-9]*) printf '%s\n' 'length must be a non-negative integer' >&2; exit 2 ;;
esac
offset=$(( 10#$offset ))
length=$(( 10#$length ))
if (( offset + length > ${#text} )); then
  printf '%s\n' 'requested range exceeds the string' >&2
  exit 2
fi
part=${text:offset:length}

この検証も整数範囲やBash版に依存します。巨大な数を受け入れる必要は通常ないため、業務上の上限を設けます。秘密値の部分取得をエラー文へ表示しません。

未設定と空文字を先に確認する

set -uが有効なスクリプトで未設定変数を部分展開するとエラーになります。必須値なら${text:?message}や[[ -v text ]]で確認し、空文字を許すかを決めます。${text-}で空を既定にすると、未設定と空文字を区別できなくなるので、要件に合わせます。

if [[ ! -v text ]]; then
  printf '%s\n' 'text is not configured' >&2
  exit 2
fi
if (( ${#text} < 3 )); then
  printf '%s\n' 'text is too short' >&2
  exit 2
fi

区切りより前後を取る#と%

固定位置ではなくパターンで前後を取りたい場合、${parameter#pattern}は先頭側で最短一致するパターンを除き、##は最長一致を除きます。%と%%は末尾側を除きます。これは正規表現ではなくBashのパターンマッチです。パターン文字を含む外部入力をそのまま使うと一致範囲が変わるため、固定した構文に限定します。

path='/srv/app/config/settings.json'
file_name=${path##*/}
directory=${path%/*}
base=${file_name%.*}
extension=${file_name##*.}
printf 'directory=%s file=%s base=%s extension=%s\n' "$directory" "$file_name" "$base" "$extension"

スラッシュがないパス、末尾スラッシュ、先頭ドット、複数ドットでは結果が変わります。ファイルパスをセキュリティ境界内に収める処理は文字列切り出しだけに頼らず、realpath、所有者、シンボリックリンク、基準ディレクトリを検証します。

置換展開との違い

${parameter/pattern/string}は一致部分を置換します。部分文字列取得とは異なり、パターンマッチの規則を使います。外部入力をpatternや置換文字列へ入れると、スラッシュ、アンパサンド、バックスラッシュなどをどう扱うか混乱しやすいため、単純な固定置換へ限定します。複雑な構造化テキストには専用パーサーを使います。

配列のスライス

配列では${array[@]:offset:length}で要素の一部を展開できます。ダブルクォートした"${array[@]:1:2}"は各要素を別々の単語として保ちます。文字列の部分取得と見た目が似ていますが、単位は文字ではなく配列要素です。疎な添字や連想配列では期待する順序と異なる場合があるため、インデックス配列の連続要素で使います。

items=('zero' 'one item' 'two' 'three')
subset=("${items[@]:1:2}")
printf '<%s>\n' "${subset[@]}"

位置パラメーターのスライス

${@:offset:length}はスクリプトや関数の引数の一部を取り出します。位置パラメーターではoffset 0に$0が関係するなど配列と差があるため、公式仕様を確認します。オプション解析後の残り引数を扱うなら、shiftで進めて"$@"を渡す方が読みやすいこともあります。引数境界を保つため必ず引用します。

日本語とロケール

UTF-8ロケールでは、日本語を1文字単位として部分取得できるのが一般的です。Cロケールではバイト単位に近い結果となり、マルチバイト文字の途中を切って不正なUTF-8を作る可能性があります。LANGとLC_CTYPEを確認し、入力のエンコーディングを固定します。別環境で同じ結果が必要なら、ロケールを要件として記録し、テストします。

text='長野県諏訪市'
printf 'first three=%s\n' "${text:0:3}"

絵文字と結合文字

Bashが数える「文字」は、利用者が見る一つの書記素とは限りません。アクセントが基底文字と結合記号に分かれている場合や、複数コードポイントをゼロ幅接合子でつなぐ絵文字では、部分取得が見た目の文字を途中で分断します。ユーザー名の表示切り詰め、SNS投稿、UIラベルには、Unicode書記素分割に対応したアプリケーション言語のライブラリを使います。端末表示幅も全角・結合文字で別の計算です。

cutを使う場合

ファイルや標準入力の各行から列を切り出すならGNU cutが使えます。-cは文字位置、-bはバイト位置、-dと-fは区切り文字とフィールドを指定します。ただし、CSVは引用符内に区切りや改行を含められるため、単純なcutでは正しく扱えません。CSVパーサーを使います。

printf '%s\n' 'abcdef' | cut -c 2-4
printf '%s\n' 'alpha:beta:gamma' | cut -d ':' -f 2

cutの位置は通常1始まりで、Bashのsubstringの0始まりと異なります。バイト位置でUTF-8を切ると文字を壊す可能性があります。末尾改行の有無、複数行、NUL区切り対応も対象版のマニュアルを確認します。

sedやawkを選ぶ場面

各行で条件付きの抽出や複数フィールド処理を行うならawk、正規表現で前後を取り除くならsedが候補です。しかし、URL、JSON、XML、CSV、ログの複数行イベントは専用パーサーの方が正確です。短い一行で済むことより、入力形式の全ケースを正しく扱えることを優先します。

境界値テスト

  • 未設定、空文字、1文字、offset 0、末尾ちょうど、範囲外。
  • 負のoffset、length 0、負のlength、非常に大きな整数。
  • 空白、タブ、改行、ワイルドカード、先頭ハイフン、スラッシュ。
  • ASCII、日本語、結合文字、絵文字、異なるUnicode正規化。
  • 区切りなし、区切りが複数、先頭・末尾が区切り、先頭ドットの名前。
  • Cロケールと本番UTF-8ロケール、対象Bash版、別シェル起動。

固定位置の部分取得は${text:offset:length}、区切りパターンの前後は#や%、各行の列ならcutというように使い分けます。外部入力の数値とパターンを検証し、Unicodeの見た目の文字を扱う用途ではBashだけに頼らないことが安全です。

公式情報・参考資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次