【问题标题】:Storing and walking a tree (with lists as leaves) in bash在 bash 中存储和行走一棵树(列表为叶子)
【发布时间】:2015-08-31 22:47:09
【问题描述】:

我想知道一种基于统一格式的文件名对数据进行分类的方法。使用像 1_dog_yorkshire.sh1_cat_persian.sh 这样的文件名,可以用简单的正则表达式表示:

[0-9]+_[a-z]+_[a-z]+.sh

我想制作如下所示的树状结构:

1 --- dog ---- yorkshire
|  |       \
|  |        -- golden retriever
|  |
|  -- cat ---- persian
|          \
|           -- siamese
|
2 --- spider ---- tarantula

想到的第一个解决方案是多维关联数组。但是,bash 不支持多维数组。哈希表也不是完美的解决方案,因为在 Bash 中对哈希表进行迭代可能会出现问题。在 Bash 中使用 XML/JSON 是不可能的,除非它是可移植的并且是用 bash 编写的。

在理想情况下,任何数据都应该是可迭代的,例如:对于“2”中的每个条目、“1”中的每只狗或“2”中蜘蛛中的蜘蛛列表中的元素。

在 Bash 中,如何构建一个足以替代多维关联数组的结构,其中子树可以被遍历,叶子可以存储列表?

【问题讨论】:

  • 可以说这是关于多维数组的问题的重复——毕竟,如果可以表示多维数组,那么任意树肯定也可以,因为可以表示多维数组表示为任意树情况的子集。因此,如果回答“可以表示二维数组吗?”是“否”,那么这个问题的答案肯定也是否定的(事实上就是这样)。
  • 顺便说一句,如果您专注于实际用例,这可能是一个更好的问题。例如,可以迭代具有给定前缀的变量列表,因此如果您的目标是遍历子树,您可以通过创建具有从文件名映射的名称的环境变量(在选定的前缀以隔离命名空间)并要求 bash 列出具有与相关子树关联的前缀的变量名称。
  • (re: 从键映射到列表——我可以想到两种方法来做到这一点,这两种方法都很糟糕:维护一个从键名/文件名到 printf %q 生成的单独关联数组具有数组内容的 eval 安全字符串,或每个名称的单独关联数组变量;bash 4.3 namerefs 可用于使实现此操作所涉及的一些代码不那么危险)。
  • ...但实际上,您最好将内容存储在 JSON 中并调用 jq 来查询它,或者使用 XML 并使用 xmlstarlet 等;这样你就有了真正的、原生的嵌套数据结构。
  • 顺便说一句,您也可以在编辑时解释为什么要在内存中执行此操作而不是通过查询文件系统,这肯定会是更简洁的方法。

标签: bash multidimensional-array data-structures data-representation


【解决方案1】:

下面是一个 hack,但是……嗯,这是众所周知的。 :)

让我们从设置测试数据集开始:

for f in 1_{dog_{yorkshire,"golden retriever"},cat_{persian,siamese}}.sh \
         2_spider_tarantula.sh; do
  echo "$f" >"$f"
done

然后我们可以为每个文件建立一个环境变量,其中包含一个内容数组:

# encode name to be a valid shell variable
translate_name() {
  local -a components
  local val retval

  IFS=_ read -r -a components <<<"$1"
  for component in "${components[@]}"; do
    val=$(printf '%s' "$component" | base64 - -)
    val_eqs=${val//[!=]/}
    val_eqs_count=${#val_eqs}
    val_no_eqs=${val//=/}
    printf -v retval '%s%s_%s__' "$retval" "$val_no_eqs" "$val_eqs_count"
  done
  printf '%s\n' "${retval%__}"
}

for f in *.sh; do
  varname=$(translate_name "${f%.sh}")
  mapfile -t "CONTENT_$varname" <"$f"
done

那么,假设您想遍历子树。

您可以列出与该子树关联的数组变量:

get_subtree_vars() {
  local subst varname

  varname=CONTENT_$(IFS=_; translate_name "$*")
  printf -v subst '"${!'"$varname"'@}"'
  eval 'printf  "%s\n" "'"$subst"'"'
}

...并将它们转换回密钥:

# given an encoded variable name, return its original name
# inverse of translate_name
get_name() {
  local varname section
  local -a sections
  for varname; do
    retval=
    varname=${varname#CONTENT_}
    varname=${varname//__/ }
    IFS=' ' read -r -a sections <<<"$varname"
    for section in "${sections[@]}"; do
      val_eqs_count=${section##*_}
      val_no_eqs=${section%_*}
      val=$val_no_eqs
      for (( i=0; i<val_eqs_count; i++ )); do
        val+="="
      done
      retval+=$(base64 -D - - <<<"$val")_
    done
    printf '%s\n' "${retval%_}"
  done
}

...并检索它们的值:

# given an encoded name, retrieve a NUL-delimited list of values stored
# this could be done much more safely with bash 4.3+ using namerefs
get_values() {
  local name cmd
  local -a values
  for name; do
    [[ $name = CONTENT_* ]] || name=CONTENT_$name
    printf -v cmd 'values=( "${%q[@]}" )' "$name" && eval "$cmd"
    printf '%s\0' "${values[@]}"
  done
}

# given a name, call a function for each leaf value associated
call_for_each() {
  local funcname=$1; shift
  while IFS= read -u 3 -r subtree_var; do
    while IFS= read -u 4 -r -d '' value; do
      "$funcname" "$value"
    done 4< <(get_values "$subtree_var")
  done 3< <(get_subtree_vars "$@")
}

因此:

printfunc() { printf '%q\n' "$@"; }
call_for_each printfunc 1 cat

...将发出:

1_cat_siamese.sh
1_cat_persian.sh

值得注意的是,这些是数据,而不是元数据——注意 .sh 扩展名,我们在创建时从变量中剥离了它!

另外一个说明:上面代码中的eval 使用应该是安全的,因为使用base64 编码来清理任何可能的shell 转义尝试(以及通过恶意文件名进行的shell 注入攻击)。存在于文件名中; printf %q 使用提供了一个附加层。在不存在这些保证的任何情况下,请谨慎部署上述方法。


综上所述——通过将内容读入内存,上述内容使事情变得非常复杂。考虑以下独立代码作为上述示例的替代方案:

get_subtree_files() {
  local prefix
  local -a files
  prefix=$(IFS=_; printf '%s\n' "$*")
  files=( "$prefix"* )

  # note that the test only checks the first entry of the array
  # ...but that's good enough to detect the no-matches case.
  [[ -e $files ]] && printf '%s\0' "${files[@]}"
}

xargs -0 cat < <(get_subtree_files 1 cat)

【讨论】:

    猜你喜欢
    • 2013-12-03
    • 1970-01-01
    • 2017-10-27
    • 2010-12-09
    • 1970-01-01
    • 1970-01-01
    • 2018-10-24
    • 1970-01-01
    • 2011-07-27
    相关资源
    最近更新 更多